Fixed: Backend/Frontend Cleanup

pull/2/head
Qstick 5 years ago
parent 286f73f38d
commit d178dce0d3

@ -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

@ -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)

@ -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 [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrai
## License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2018
* Copyright 2010-2019

@ -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,

@ -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';
}

@ -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 (
<TableRowCell key={name}>
{outputPath}
</TableRowCell>
);
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
@ -297,6 +306,7 @@ QueueRow.propTypes = {
quality: PropTypes.object.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
outputPath: PropTypes.string,
downloadClient: PropTypes.string,
estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string,

@ -12,8 +12,12 @@ function createMapStateToProps() {
(state) => 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
};
}
);

@ -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,

@ -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,

@ -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 ?
<Menu
className={styles.viewMenu}
alignMenu={align.RIGHT}
@ -158,6 +159,18 @@ class CalendarHeader extends Component {
</MenuButton>
<MenuContent>
{
isSmallScreen ?
null :
<ViewMenuItem
name={calendarViews.MONTH}
selectedView={view}
onPress={this.onViewChange}
>
Month
</ViewMenuItem>
}
<ViewMenuItem
name={calendarViews.WEEK}
selectedView={view}
@ -243,6 +256,7 @@ CalendarHeader.propTypes = {
end: PropTypes.string.isRequired,
view: PropTypes.oneOf(calendarViews.all).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
collapseViewButtons: PropTypes.bool.isRequired,
longDateFormat: PropTypes.string.isRequired,
onViewChange: PropTypes.func.isRequired,
onTodayPress: PropTypes.func.isRequired,

@ -23,6 +23,7 @@ function createMapStateToProps() {
]);
result.isSmallScreen = dimensions.isSmallScreen;
result.collapseViewButtons = dimensions.isLargeScreen;
result.longDateFormat = uiSettings.longDateFormat;
return result;

@ -78,8 +78,8 @@
color: $disabledColor;
}
.addNewSeriesSuggestion {
padding: 0 3px;
.addNewMovieSuggestion {
padding: 5px 3px;
cursor: pointer;
}

@ -76,7 +76,7 @@ class MovieSearchInput extends Component {
renderSuggestion(item, { query }) {
if (item.type === ADD_NEW_TYPE) {
return (
<div className={styles.addNewSeriesSuggestion}>
<div className={styles.addNewMovieSuggestion}>
Search for {query}
</div>
);

@ -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

@ -32,7 +32,7 @@ function BackupSettings(props) {
<FormLabel>Folder</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
type={inputTypes.PATH}
name="backupFolder"
helpText="Relative paths will be under Radarr's AppData directory"
onChange={onInputChange}

@ -83,10 +83,6 @@ class NamingModal extends Component {
value,
isOpen,
advancedSettings,
season,
episode,
daily,
anime,
additional,
onInputChange,
onModalClose
@ -112,58 +108,21 @@ class NamingModal extends Component {
const fileNameTokens = [
{
token: '{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}',
example: 'Series Title (2010) - S01E01 - Episode Title HDTV-720p Proper'
},
{
token: '{Series Title} - {season:0}x{episode:00} - {Episode Title} {Quality Full}',
example: 'Series Title (2010) - 1x01 - Episode Title HDTV-720p Proper'
},
{
token: '{Series.Title}.S{season:00}E{episode:00}.{EpisodeClean.Title}.{Quality.Full}',
example: 'Series.Title.(2010).S01E01.Episode.Title.HDTV-720p'
token: '{Movie Title} - {Quality Full}',
example: 'Movie Title (2010) - HDTV-720p Proper'
}
];
const seriesTokens = [
{ token: '{Series Title}', example: 'Series Title!' },
{ token: '{Series CleanTitle}', example: 'Series Title' },
{ token: '{Series CleanTitleYear}', example: 'Series Title 2010' },
{ token: '{Series TitleThe}', example: 'Series Title, The' },
{ token: '{Series TitleTheYear}', example: 'Series Title, The (2010)' },
{ token: '{Series TitleYear}', example: 'Series Title (2010)' }
];
const seriesIdTokens = [
{ token: '{ImdbId}', example: 'tt12345' },
{ token: '{TvdbId}', example: '12345' },
{ token: '{TvMazeId}', example: '54321' }
];
const seasonTokens = [
{ token: '{season:0}', example: '1' },
{ token: '{season:00}', example: '01' }
];
const episodeTokens = [
{ token: '{episode:0}', example: '1' },
{ token: '{episode:00}', example: '01' }
];
const airDateTokens = [
{ token: '{Air-Date}', example: '2016-03-20' },
{ token: '{Air Date}', example: '2016 03 20' }
];
const movieTokens = [
{ token: '{Movie Title}', example: 'Movie Title!' },
{ token: '{Movie CleanTitle}', example: 'Movie Title' },
{ token: '{Movie TitleThe}', example: 'Movie Title, The' }
const absoluteTokens = [
{ token: '{absolute:0}', example: '1' },
{ token: '{absolute:00}', example: '01' },
{ token: '{absolute:000}', example: '001' }
];
const episodeTitleTokens = [
{ token: '{Episode Title}', example: 'Episode Title' },
{ token: '{Episode CleanTitle}', example: 'Episode Title' }
const movieIdTokens = [
{ token: '{ImdbId}', example: 'tt12345' },
{ token: '{TmdbId}', example: '123456' }
];
const qualityTokens = [
@ -175,8 +134,14 @@ class NamingModal extends Component {
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' },
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo AudioFormat}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' }
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]' },
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]' },
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
{ token: '{MediaInfo VideoDynamicRange}', example: 'HDR' }
];
const releaseGroupTokens = [
@ -184,8 +149,8 @@ class NamingModal extends Component {
];
const originalTokens = [
{ token: '{Original Title}', example: 'Series.Title.S01E01.HDTV.x264-EVOLVE' },
{ token: '{Original Filename}', example: 'series.title.s01e01.hdtv.x264-EVOLVE' }
{ token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' },
{ token: '{Original Filename}', example: 'Movie.title.hdtv.x264-EVOLVE' }
];
return (
@ -244,10 +209,10 @@ class NamingModal extends Component {
</FieldSet>
}
<FieldSet legend="Series">
<FieldSet legend="Movie">
<div className={styles.groups}>
{
seriesTokens.map(({ token, example }) => {
movieTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
@ -266,10 +231,10 @@ class NamingModal extends Component {
</div>
</FieldSet>
<FieldSet legend="Series ID">
<FieldSet legend="Movie ID">
<div className={styles.groups}>
{
seriesIdTokens.map(({ token, example }) => {
movieIdTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
@ -288,133 +253,9 @@ class NamingModal extends Component {
</div>
</FieldSet>
{
season &&
<FieldSet legend="Season">
<div className={styles.groups}>
{
seasonTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
{
episode &&
<div>
<FieldSet legend="Episode">
<div className={styles.groups}>
{
episodeTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
{
daily &&
<FieldSet legend="Air-Date">
<div className={styles.groups}>
{
airDateTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
{
anime &&
<FieldSet legend="Absolute Episode Number">
<div className={styles.groups}>
{
absoluteTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
</div>
}
{
additional &&
<div>
<FieldSet legend="Episode Title">
<div className={styles.groups}>
{
episodeTitleTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
<FieldSet legend="Quality">
<div className={styles.groups}>
{
@ -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
};

@ -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;

@ -84,7 +84,7 @@ class Notification extends Component {
{
supportsOnDownload && onDownload &&
<Label kind={kinds.SUCCESS}>
On Download
On Import
</Label>
}

@ -19,6 +19,11 @@ class MoreInfo extends Component {
<Link to="https://radarr.video/">radarr.video</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://discord.gg/AD3UP37">discord.gg/AD3UP37</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Wiki</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://github.com/Radarr/Radarr/wiki">github.com/Radarr/Radarr/wiki</Link>

@ -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<TDispatcher> : TestBase<HttpClient> 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<IPlatformInfo>().Setup(c => c.Version).Returns(new Version("1.0.0"));
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
@ -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<HttpBinResource>(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<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.GetMock<IUserAgentBuilder>().Object, Mocker.Resolve<Logger>());
@ -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<HttpBinResource>(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<HttpBinResource>(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<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(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<string, object> Args { get; set; }
public Dictionary<string, object> Headers { get; set; }
public string Origin { get; set; }
public string Url { get; set; }

@ -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<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))

@ -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<Nyaa>();
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<Nyaa>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new NyaaSettings()
};
var result = indexer.Fetch(_singleSearchCriteria);
ValidateTorrentResult(result, hasSize: true);
}
private void ValidateTorrentResult(IList<ReleaseInfo> reports, bool hasSize = false, bool hasInfoUrl = false, bool hasMagnet = false)
{
reports.Should().OnlyContain(c => c.GetType() == typeof(TorrentInfo));

@ -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<DownloadedEpisodesCommandService>
{
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<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessRootFolder(It.IsAny<DirectoryInfo>()))
.Returns(new List<ImportResult>());
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>());
var downloadItem = Builder<DownloadClientItem>.CreateNew()
.With(v => v.DownloadId = "sab1")
.With(v => v.Status = DownloadItemStatus.Downloading)
.Build();
var remoteEpisode = Builder<RemoteEpisode>.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<IDiskProvider>().Setup(c => c.FolderExists(It.IsAny<string>()))
.Returns(true);
}
private void GivenExistingFile(string path)
{
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(It.IsAny<string>()))
.Returns(true);
}
private void GivenValidQueueItem()
{
Mocker.GetMock<ITrackedDownloadService>()
.Setup(s => s.Find("sab1"))
.Returns(_trackedDownload);
}
[Test]
public void should_skip_import_if_dronefactory_doesnt_exist()
{
Assert.Throws<ArgumentException>(() => Subject.Execute(new DownloadedEpisodesScanCommand()));
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessRootFolder(It.IsAny<DirectoryInfo>()), Times.Never());
}
[Test]
public void should_process_folder_if_downloadclientid_is_not_specified()
{
GivenExistingFolder(_downloadFolder);
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), 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<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), 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<IDownloadedEpisodesImportService>().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<IDownloadedEpisodesImportService>().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<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), 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<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Copy, null, null), Times.Once());
}
}
}

@ -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<DownloadedEpisodesImportService>
{
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<IDiskScanService>().Setup(c => c.GetVideoFiles(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(_videoFiles);
Mocker.GetMock<IDiskProvider>().Setup(c => c.GetDirectories(It.IsAny<string>()))
.Returns(_subFolders);
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
}
private void GivenValidSeries()
{
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetSeries(It.IsAny<string>()))
.Returns(Builder<Series>.CreateNew().Build());
}
[Test]
public void should_search_for_series_using_folder_name()
{
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IParsingService>().Verify(c => c.GetSeries("foldername"), Times.Once());
}
[Test]
public void should_skip_if_file_is_in_use_by_another_process()
{
GivenValidSeries();
Mocker.GetMock<IDiskProvider>().Setup(c => c.IsFileLocked(It.IsAny<string>()))
.Returns(true);
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
VerifyNoImport();
}
[Test]
public void should_skip_if_no_series_found()
{
Mocker.GetMock<IParsingService>().Setup(c => c.GetSeries("foldername")).Returns((Series)null);
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IMakeImportDecision>()
.Verify(c => c.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()),
Times.Never());
VerifyNoImport();
}
[Test]
public void should_not_import_if_folder_is_a_series_path()
{
GivenValidSeries();
Mocker.GetMock<ISeriesService>()
.Setup(s => s.SeriesPathExists(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskScanService>()
.Setup(c => c.GetVideoFiles(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(new string[0]);
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IDiskScanService>()
.Verify(v => v.GetVideoFiles(It.IsAny<string>(), true), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_delete_folder_if_no_files_were_imported()
{
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetFolderSize(It.IsAny<string>()), 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<ImportDecision>();
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), null, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.DeleteFolder(It.IsAny<string>(), 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<ImportDecision>();
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), null, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Mocker.GetMock<IDetectSample>()
.Setup(s => s.IsSample(It.IsAny<Series>(),
It.IsAny<QualityModel>(),
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<bool>()))
.Returns(true);
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.DeleteFolder(It.IsAny<string>(), 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<IDiskProvider>()
.Setup(c => c.GetDirectories(It.IsAny<string>()))
.Returns(folders);
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetSeries(folderName), Times.Once());
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetSeries(It.Is<string>(s => s.StartsWith(prefix))), Times.Never());
}
[Test]
public void should_return_importresult_on_unknown_series()
{
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists(It.IsAny<string>()))
.Returns(false);
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(It.IsAny<string>()))
.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<ImportDecision>();
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), null, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Mocker.GetMock<IDetectSample>()
.Setup(s => s.IsSample(It.IsAny<Series>(),
It.IsAny<QualityModel>(),
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<bool>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetFiles(It.IsAny<string>(), SearchOption.AllDirectories))
.Returns(new []{ _videoFiles.First().Replace(".ext", ".rar") });
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetFileSize(It.IsAny<string>()))
.Returns(15.Megabytes());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.DeleteFolder(It.IsAny<string>(), 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<IDiskProvider>().Setup(c => c.FolderExists(folderName))
.Returns(true);
Mocker.GetMock<IDiskProvider>().Setup(c => c.GetFiles(folderName, SearchOption.TopDirectoryOnly))
.Returns(new[] { fileName });
var localEpisode = new LocalEpisode();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localEpisode));
Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.Is<ParsedEpisodeInfo>(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<IDiskProvider>().Setup(c => c.FolderExists(fileName))
.Returns(false);
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(fileName))
.Returns(true);
var localEpisode = new LocalEpisode();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localEpisode));
var result = Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), 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<IDiskProvider>().Setup(c => c.FolderExists(folderName))
.Returns(false);
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(folderName))
.Returns(false);
Subject.ProcessPath(folderName).Should().BeEmpty();
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetSeries(It.IsAny<string>()), 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<ImportDecision>();
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), null, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
Mocker.GetMock<IDetectSample>()
.Setup(s => s.IsSample(It.IsAny<Series>(),
It.IsAny<QualityModel>(),
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<bool>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetFileSize(It.IsAny<string>()))
.Returns(15.Megabytes());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.DeleteFolder(It.IsAny<string>(), true), Times.Never());
}
private void VerifyNoImport()
{
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Times.Never());
}
private void VerifyImport()
{
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Times.Once());
}
}
}

@ -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");

@ -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("", "")]

@ -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<ImageFileResult>();
}
var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType);

@ -110,7 +110,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
if (movie == null)
{
movie = trackedDownload.RemoteMovie.Movie;
movie = trackedDownload.RemoteMovie?.Movie;
}
}

@ -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<DiscordSettings>
{
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<Embed>
{
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<Embed>
{
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<ValidationFailure>();
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<Embed> 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;
}
}
}

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Notifications.Discord
{
public enum DiscordColors
{
Danger = 15749200,
Success = 2605644,
Warning = 16753920
}
}

@ -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)
{
}
}
}

@ -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);
}
}
}
}

@ -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<DiscordSettings>
{
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));
}
}
}

@ -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<Embed> Embeds { get; set; }
}
}

@ -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; }
}
}

@ -1,4 +1,5 @@
namespace NzbDrone.Core.Notifications.Gotify
{
public enum GotifyPriority
{

@ -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<SlackSettings>
{
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";

@ -964,10 +964,17 @@
<Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="MetadataSource\TmdbConfigurationService.cs" />
<Compile Include="NetImport\NetImportSyncCommand.cs" />
<Compile Include="Notifications\Discord\Discord.cs" />
<Compile Include="Notifications\Discord\DiscordColors.cs" />
<Compile Include="Notifications\Discord\DiscordException.cs" />
<Compile Include="Notifications\Discord\DiscordProxy.cs" />
<Compile Include="Notifications\Discord\DiscordSettings.cs" />
<Compile Include="Notifications\Discord\Payloads\DiscordPayload.cs" />
<Compile Include="Notifications\Discord\Payloads\Embed.cs" />
<Compile Include="Notifications\Gotify\GotifyProxy.cs" />
<Compile Include="Notifications\Gotify\InvalidResponseException.cs" />
<Compile Include="Notifications\Gotify\Gotify.cs" />
<Compile Include="Notifications\Gotify\GotifyPriority.cs" />
<Compile Include="Notifications\Gotify\GotifyService.cs" />
<Compile Include="Notifications\Gotify\GotifySettings.cs" />
<Compile Include="Notifications\Join\JoinAuthException.cs" />
<Compile Include="Notifications\Join\JoinInvalidDeviceException.cs" />

@ -35,18 +35,9 @@ namespace NzbDrone.Core.Organizer
private static readonly Regex TitleRegex = new Regex(@"\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9]+))?(?<suffix>[- ._)\]]*)\}",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex TagsRegex = new Regex(@"(?<tags>\{tags(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?<separator>(?<=})[- ._]+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>[- ._]?[ex])(?<episode>{episode(?:\:0+)?}))(?<separator>[- ._]+?(?={))?",
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<Match>()
.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)

@ -113,7 +113,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[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)\]$",

@ -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();

@ -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]

@ -26,4 +26,4 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo
info.Version.Should().NotBeNullOrWhiteSpace();
}
}
}
}

@ -74,4 +74,4 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters
.Verify(c => c.GetFiles(It.IsAny<string>(), SearchOption.TopDirectoryOnly), Times.Never());
}
}
}
}

@ -79,4 +79,4 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters
}
}
}
}

@ -10,6 +10,7 @@ namespace NzbDrone.Mono.Disk
{
{ "afpfs", DriveType.Network },
{ "apfs", DriveType.Fixed },
{ "fuse.mergerfs", DriveType.Fixed },
{ "zfs", DriveType.Fixed }
};

@ -88,7 +88,7 @@
<Compile Include="SignalRDependencyResolver.cs" />
<Compile Include="SignalRJsonSerializer.cs" />
<Compile Include="SignalRMessage.cs" />
<Compile Include="SonarrPerformanceCounterManager.cs" />
<Compile Include="RadarrPerformanceCounterManager.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\NzbDrone.Common.csproj">

@ -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();

@ -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);
}

@ -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<int>();
// 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<Event>();
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);

@ -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<RemoteMovie>(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");
}

@ -56,10 +56,10 @@ namespace Radarr.Api.V2.Indexers
if (firstDecision?.RemoteMovie.ParsedMovieInfo == null)
{
throw new ValidationException(new List<ValidationFailure> { new ValidationFailure("Title", "Unable to parse", release.Title) });
throw new ValidationException(new List<ValidationFailure>{ 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;
}
}

@ -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

Loading…
Cancel
Save