1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

...

45 Commits

Author SHA1 Message Date
Qstick 793cf22c3b WIP: New: Exclusion Lists 2020-10-17 02:16:17 -04:00
Qstick 76cabb4927 Use new multi-select component for indexer languages 2020-10-17 01:38:41 -04:00
nitsua 5b83d09d5e Remove some instances of moment that are not needed on the index to reduce the load 2020-10-17 01:04:50 -04:00
Daniel Martin Gonzalez 82eadcffaa New: Add Option to localize metadata written in .nfo if available (#5060)
* Add Option to localize metadata written in .nfo if available

* Fix Pull Request comments
2020-10-17 01:03:46 -04:00
Qstick 1ebb71db59 Fixed: Import Extra files from Subfolders
Fixes: #4646
2020-10-17 00:17:38 -04:00
Qstick 662b3894c2 Fixed: Make Notification trigger texts consistent
Fixes #5012
2020-10-17 00:06:23 -04:00
Qstick a1c21af9b5 Fixed: (Windows) clean up extraneous files in build folder during installation
Fixes #5200
Fixes #5203

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 23:33:03 -04:00
Qstick 67fca87c44 Added PrivacyLevel option to FieldDefinition for later usage
Fixes #5107

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2020-10-16 23:27:01 -04:00
Qstick 6ee2780370 Fixed some mediainfo subtitle codes
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 23:06:17 -04:00
Qstick 0086e2699e Protect against undefined SizeOnDisk
Fixes #5205

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:59:21 -04:00
Qstick 37c9701237 Fixed: Rerender Overviews on Overview option changes
Fixes #5150

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:55:28 -04:00
Qstick 6d452d8479 New: Bulk remove from Blacklist
Fixes #5194

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:45:23 -04:00
Qstick e82f0c01d8 Re-saving edited providers will forcibly save them
Fixes #5192

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:28:19 -04:00
Qstick ee456c3291 Fixed: MapCoversToLocal for tmdbid queries to all movies endpoint 2020-10-16 22:19:17 -04:00
kingii98 9986f0119b Translated using Weblate (Russian) [skip ci]
Currently translated at 6.7% (57 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/ru/
2020-10-16 21:48:58 +00:00
Csaba 5d4da26195 Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (844 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-16 21:48:58 +00:00
memnos 5ed448c930 Translated using Weblate (Italian) [skip ci]
Currently translated at 100.0% (844 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/it/
2020-10-16 21:48:58 +00:00
jpalenz77 9263e31b7b Translated using Weblate (Spanish) [skip ci]
Currently translated at 100.0% (844 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/es/
2020-10-16 21:48:57 +00:00
angrycuban13 516122c6f3 Translated using Weblate (Spanish) [skip ci]
Currently translated at 100.0% (843 of 843 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/es/
2020-10-16 03:00:36 +00:00
foXaCe 256a50abab Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (843 of 843 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-16 03:00:36 +00:00
EnorMOZ 315929bc5e Update "TagsHelpText" with similar language as sonarr (#5218)
* Update "TagsHelpText" with similar language as sonarr

* Use new translation key
2020-10-15 23:00:31 -04:00
Qstick 4a681601b2 Skip Screenshot on 2nd build attempt 2020-10-15 21:14:27 -04:00
Csaba ab3b5bdf8b Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (842 of 842 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-15 14:38:36 +00:00
Qstick e52288bd67 New: Edit RlsGroup, Flags, and Edition for Movie Files (#5183)
* New: Edit RlsGroup and Edition for Movie Files

* fixup! remove console log

* fixup! translation
2020-10-15 10:38:30 -04:00
servarr[bot] f2f26d88b9 New: Differentiate between short term and long term (more than 6 hours) indexer failures (#5202)
* New: Differentiate between short term and long term (more than 6 hours) indexer failures

(cherry picked from commit 2adedb97da5ad31b65f0dc2eec5c263efe95731f)

* fixup! Mock Localization

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
Co-authored-by: Qstick <qstick@gmail.com>
2020-10-15 07:20:02 -04:00
nitsua 27354507cb Fix issue with IMDb lists (some not all) not parsing properly causing an issue when trying to add them 2020-10-14 15:08:08 -04:00
Qstick b7aa1df219 Fix Automation/cleanup build yml (#5211)
* Cleanup build yml

* fixup! bump chrome driver
2020-10-13 21:53:17 -04:00
ta264 2d7942d69c Fixed: Speed up RSS sync 2020-10-13 20:54:33 -04:00
ta264 0a8dd85856 Fixed: Speed up initial movie load when opening the UI 2020-10-13 20:54:33 -04:00
ta264 f917d0e9bc Add FileInfo utility functions to DiskProvider 2020-10-13 20:54:33 -04:00
kingii98 024e4df99c Translated using Weblate (Russian) [skip ci]
Currently translated at 6.1% (52 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/ru/
2020-10-13 16:48:58 +00:00
Csaba 49fa402c55 Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-13 16:48:57 +00:00
Qstick 02c95658c4 Windows installer improvements
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-11 22:43:08 -04:00
Mark McDowall 3bc4231640 New: Health events for Webhooks
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-11 22:40:18 -04:00
Mark McDowall a9a0d47f9f Fixed: Copying passwords
(cherry picked from commit c871b3f9487b9bfeb3726d763a632a772b420a0a)
2020-10-11 20:36:46 -04:00
Florian 0dd05b2dac Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-11 21:48:51 +00:00
Qstick 3986433884 Fixed: Default a Movie object with Empty Ratings 2020-10-11 01:26:53 -04:00
Mark McDowall f637976530 Fixed: Parsing of URLs with double slashes in the path
(cherry picked from commit 0c7743e786749b333333d282412ff76fc10aba65)
2020-10-10 17:16:21 -04:00
Qstick 603d26bb5f Take Screenshot on Automation tests for build status notifications 2020-10-09 21:51:59 -04:00
Will Segatto 6b41ad7442 Translated using Weblate (Portuguese) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/pt/
2020-10-10 00:49:05 +00:00
hotio 4c049ac3d9 Translated using Weblate (Dutch) [skip ci]
Currently translated at 97.9% (823 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/nl/
2020-10-10 00:49:04 +00:00
Csaba 84df3e8b5b Translated using Weblate (Hungarian) [skip ci]
Currently translated at 91.4% (768 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-10 00:49:03 +00:00
jpalenz77 0ff76e14bb Translated using Weblate (Spanish) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/es/
2020-10-10 00:48:55 +00:00
Florian 5433c6364c Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-10 00:48:50 +00:00
geogolem 038e3d5c44 update trayIcon with new Radarr Icon 2020-10-08 14:27:07 -04:00
156 changed files with 2460 additions and 428 deletions
+19 -20
View File
@@ -386,11 +386,6 @@ stages:
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
wget https://github.com/acoustid/chromaprint/releases/download/v1.4.3/chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz
sudo tar xf chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz --strip-components=1 --directory /usr/bin
displayName: Install fpcalc
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- bash: | - bash: |
SYMLINK=6_6_0 SYMLINK=6_6_0
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
@@ -693,24 +688,21 @@ stages:
mkdir -p ./bin/ mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/ cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents displayName: Move Package Contents
- bash: |
if [[ $OSNAME == "Mac" ]]; then
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-macos.tar.gz
elif [[ $OSNAME == "Linux" ]]; then
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-linux64.tar.gz
else
echo "Unhandled OS"
exit 1
fi
curl -s -L "$url" | tar -xz
chmod +x geckodriver
mv geckodriver _tests
displayName: Install Gecko Driver
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: | - bash: |
chmod a+x ${TESTSFOLDER}/test.sh chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test ${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
displayName: Run Automation Tests displayName: Run Automation Tests
- task: CopyFiles@2
displayName: 'Copy Screenshot to: $(Build.ArtifactStagingDirectory)'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*_test_screenshot.png
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots'
displayName: Publish Screenshot Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
- task: PublishTestResults@2 - task: PublishTestResults@2
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
@@ -859,8 +851,15 @@ stages:
- job: - job:
displayName: Discord Notification displayName: Discord Notification
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: 'windows-2019'
steps: steps:
- task: DownloadPipelineArtifact@2
continueOnError: true
displayName: Download Screenshot Artifact
inputs:
buildType: 'current'
artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory)
- checkout: none - checkout: none
- powershell: | - powershell: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1')) iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
+110 -1
View File
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -10,12 +11,84 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlacklistRowConnector from './BlacklistRowConnector'; import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component { class Blacklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
// //
// Render // Render
@@ -27,15 +100,33 @@ class Blacklist extends Component {
items, items,
columns, columns,
totalRecords, totalRecords,
isRemoving,
isClearingBlacklistExecuting, isClearingBlacklistExecuting,
onClearBlacklistPress, onClearBlacklistPress,
...otherProps ...otherProps
} = this.props; } = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return ( return (
<PageContent title={translate('Blacklist')}> <PageContent title={translate('Blacklist')}>
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton <PageToolbarButton
label={translate('Clear')} label={translate('Clear')}
iconName={icons.CLEAR} iconName={icons.CLEAR}
@@ -81,8 +172,12 @@ class Blacklist extends Component {
isPopulated && !error && !!items.length && isPopulated && !error && !!items.length &&
<div> <div>
<Table <Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns} columns={columns}
{...otherProps} {...otherProps}
onSelectAllChange={this.onSelectAllChange}
> >
<TableBody> <TableBody>
{ {
@@ -90,8 +185,10 @@ class Blacklist extends Component {
return ( return (
<BlacklistRowConnector <BlacklistRowConnector
key={item.id} key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns} columns={columns}
{...item} {...item}
onSelectedChange={this.onSelectedChange}
/> />
); );
}) })
@@ -107,6 +204,16 @@ class Blacklist extends Component {
</div> </div>
} }
</PageContentBody> </PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blacklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent> </PageContent>
); );
} }
@@ -119,7 +226,9 @@ Blacklist.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number, totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired, isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired onClearBlacklistPress: PropTypes.func.isRequired
}; };
@@ -89,6 +89,10 @@ class BlacklistConnector extends Component {
this.props.gotoBlacklistPage({ page }); this.props.gotoBlacklistPage({ page });
} }
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
}
onSortPress = (sortKey) => { onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey }); this.props.setBlacklistSort({ sortKey });
} }
@@ -124,6 +128,7 @@ class BlacklistConnector extends Component {
onNextPagePress={this.onNextPagePress} onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress} onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect} onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress} onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange} onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress} onClearBlacklistPress={this.onClearBlacklistPress}
@@ -143,6 +148,7 @@ BlacklistConnector.propTypes = {
gotoBlacklistNextPage: PropTypes.func.isRequired, gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired, gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired, gotoBlacklistPage: PropTypes.func.isRequired,
removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired, setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired, setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired, clearBlacklist: PropTypes.func.isRequired,
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
@@ -42,6 +43,7 @@ class BlacklistRow extends Component {
render() { render() {
const { const {
id,
movie, movie,
sourceTitle, sourceTitle,
quality, quality,
@@ -51,7 +53,9 @@ class BlacklistRow extends Component {
protocol, protocol,
indexer, indexer,
message, message,
isSelected,
columns, columns,
onSelectedChange,
onRemovePress onRemovePress
} = this.props; } = this.props;
@@ -61,6 +65,12 @@ class BlacklistRow extends Component {
return ( return (
<TableRow> <TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{ {
columns.map((column) => { columns.map((column) => {
const { const {
@@ -194,7 +204,9 @@ BlacklistRow.propTypes = {
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired onRemovePress: PropTypes.func.isRequired
}; };
@@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions'; import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector';
import BlacklistRow from './BlacklistRow'; import BlacklistRow from './BlacklistRow';
@@ -18,7 +18,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onRemovePress() { onRemovePress() {
dispatch(removeFromBlacklist({ id: props.id })); dispatch(removeBlacklistItem({ id: props.id }));
} }
}; };
} }
@@ -10,6 +10,7 @@ import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector'; import DeviceInputConnector from './DeviceInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput';
import FormInputHelpText from './FormInputHelpText'; import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput'; import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
import NumberInput from './NumberInput'; import NumberInput from './NumberInput';
@@ -66,6 +67,9 @@ function getComponent(type) {
case inputTypes.ROOT_FOLDER_SELECT: case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector; return RootFolderSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
case inputTypes.SELECT: case inputTypes.SELECT:
return EnhancedSelectInput; return EnhancedSelectInput;
@@ -0,0 +1,70 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { indexerFlags }) => indexerFlags,
(state) => state.settings.indexerFlags,
(selectedFlags, indexerFlags) => {
const value = [];
indexerFlags.items.forEach((item) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & item.id) === item.id) {
value.push(item.id);
}
});
const values = indexerFlags.items.map(({ id, name }) => {
return {
key: id,
value: name
};
});
return {
value,
values
};
}
);
}
class IndexerFlagsSelectInputConnector extends Component {
onChange = ({ name, value }) => {
let indexerFlags = 0;
value.forEach((flagId) => {
indexerFlags += flagId;
});
this.props.onChange({ name, value: indexerFlags });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerFlagsSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
indexerFlags: PropTypes.number.isRequired,
value: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps)(IndexerFlagsSelectInputConnector);
@@ -3,10 +3,17 @@ import React from 'react';
import TextInput from './TextInput'; import TextInput from './TextInput';
import styles from './PasswordInput.css'; import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
e.preventDefault();
e.nativeEvent.stopImmediatePropagation();
}
function PasswordInput(props) { function PasswordInput(props) {
return ( return (
<TextInput <TextInput
{...props} {...props}
onCopy={onCopy}
/> />
); );
} }
+5 -1
View File
@@ -130,7 +130,8 @@ class TextInput extends Component {
step, step,
min, min,
max, max,
onBlur onBlur,
onCopy
} = this.props; } = this.props;
return ( return (
@@ -155,6 +156,8 @@ class TextInput extends Component {
onChange={this.onChange} onChange={this.onChange}
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={onBlur} onBlur={onBlur}
onCopy={onCopy}
onCut={onCopy}
onKeyUp={this.onKeyUp} onKeyUp={this.onKeyUp}
onMouseDown={this.onMouseDown} onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp} onMouseUp={this.onMouseUp}
@@ -180,6 +183,7 @@ TextInput.propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func, onFocus: PropTypes.func,
onBlur: PropTypes.func, onBlur: PropTypes.func,
onCopy: PropTypes.func,
onSelectionChange: PropTypes.func onSelectionChange: PropTypes.func
}; };
+14 -1
View File
@@ -6,7 +6,7 @@ import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchMovies } from 'Store/Actions/movieActions'; import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
(state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.languages.isPopulated, (state) => state.settings.languages.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
( (
@@ -56,6 +57,7 @@ const selectIsPopulated = createSelector(
uiSettingsIsPopulated, uiSettingsIsPopulated,
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
languagesIsPopulated, languagesIsPopulated,
indexerFlagsIsPopulated,
importListsIsPopulated, importListsIsPopulated,
systemStatusIsPopulated systemStatusIsPopulated
) => { ) => {
@@ -65,6 +67,7 @@ const selectIsPopulated = createSelector(
uiSettingsIsPopulated && uiSettingsIsPopulated &&
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
languagesIsPopulated && languagesIsPopulated &&
indexerFlagsIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
systemStatusIsPopulated systemStatusIsPopulated
); );
@@ -77,6 +80,7 @@ const selectErrors = createSelector(
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
(state) => state.settings.qualityProfiles.error, (state) => state.settings.qualityProfiles.error,
(state) => state.settings.languages.error, (state) => state.settings.languages.error,
(state) => state.settings.indexerFlags.error,
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.system.status.error, (state) => state.system.status.error,
( (
@@ -85,6 +89,7 @@ const selectErrors = createSelector(
uiSettingsError, uiSettingsError,
qualityProfilesError, qualityProfilesError,
languagesError, languagesError,
indexerFlagsError,
importListsError, importListsError,
systemStatusError systemStatusError
) => { ) => {
@@ -94,6 +99,7 @@ const selectErrors = createSelector(
uiSettingsError || uiSettingsError ||
qualityProfilesError || qualityProfilesError ||
languagesError || languagesError ||
indexerFlagsError ||
importListsError || importListsError ||
systemStatusError systemStatusError
); );
@@ -105,6 +111,7 @@ const selectErrors = createSelector(
uiSettingsError, uiSettingsError,
qualityProfilesError, qualityProfilesError,
languagesError, languagesError,
indexerFlagsError,
importListsError, importListsError,
systemStatusError systemStatusError
}; };
@@ -153,6 +160,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchLanguages() { dispatchFetchLanguages() {
dispatch(fetchLanguages()); dispatch(fetchLanguages());
}, },
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchImportLists() { dispatchFetchImportLists() {
dispatch(fetchImportLists()); dispatch(fetchImportLists());
}, },
@@ -191,6 +201,7 @@ class PageConnector extends Component {
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguages(); this.props.dispatchFetchLanguages();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchImportLists(); this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
@@ -215,6 +226,7 @@ class PageConnector extends Component {
dispatchFetchTags, dispatchFetchTags,
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchLanguages, dispatchFetchLanguages,
dispatchFetchIndexerFlags,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
@@ -254,6 +266,7 @@ PageConnector.propTypes = {
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired, dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
+2
View File
@@ -10,6 +10,7 @@ export const PASSWORD = 'password';
export const PATH = 'path'; export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const SELECT = 'select'; export const SELECT = 'select';
export const TAG = 'tag'; export const TAG = 'tag';
export const TEXT = 'text'; export const TEXT = 'text';
@@ -30,6 +31,7 @@ export const all = [
PATH, PATH,
QUALITY_PROFILE_SELECT, QUALITY_PROFILE_SELECT,
ROOT_FOLDER_SELECT, ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT,
SELECT, SELECT,
TAG, TAG,
TEXT, TEXT,
+1 -1
View File
@@ -549,7 +549,7 @@ class MovieDetails extends Component {
> >
<span className={styles.sizeOnDisk}> <span className={styles.sizeOnDisk}>
{ {
formatBytes(sizeOnDisk) formatBytes(sizeOnDisk || 0)
} }
</span> </span>
</InfoLabel> </InfoLabel>
@@ -72,7 +72,8 @@ class MovieIndexOverviews extends Component {
sortKey, sortKey,
overviewOptions, overviewOptions,
jumpToCharacter, jumpToCharacter,
isMovieEditorActive isMovieEditorActive,
isSmallScreen
} = this.props; } = this.props;
const { const {
@@ -82,13 +83,15 @@ class MovieIndexOverviews extends Component {
if (prevProps.sortKey !== sortKey || if (prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions) { prevProps.overviewOptions !== overviewOptions) {
this.calculateGrid(); this.calculateGrid(this.state.width, isSmallScreen);
} }
if (this._grid && if (
this._grid &&
(prevState.width !== width || (prevState.width !== width ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) || hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.overviewOptions !== overviewOptions ||
prevProps.isMovieEditorActive !== isMovieEditorActive)) { prevProps.isMovieEditorActive !== isMovieEditorActive)) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import FileEditModalContentConnector from './FileEditModalContentConnector';
class FileEditModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<FileEditModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
FileEditModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default FileEditModal;
@@ -0,0 +1,237 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class FileEditModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
qualityId,
languageIds,
indexerFlags,
proper,
real,
edition,
releaseGroup
} = props;
this.state = {
qualityId,
languageIds,
indexerFlags,
proper,
real,
edition,
releaseGroup
};
}
//
// Listeners
onQualityChange = ({ value }) => {
this.setState({ qualityId: parseInt(value) });
}
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
}
onSaveInputs = () => {
this.props.onSaveInputs(this.state);
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
qualities,
languages,
relativePath,
onModalClose
} = this.props;
const {
qualityId,
languageIds,
indexerFlags,
proper,
real,
edition,
releaseGroup
} = this.state;
const qualityOptions = qualities.map(({ id, name }) => {
return {
key: id,
value: name
};
});
const languageOptions = languages.map(({ id, name }) => {
return {
key: id,
value: name
};
});
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('EditMovieFile')} - {relativePath}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadQualities')}
</div>
}
{
isPopulated && !error &&
<Form>
<FormGroup>
<FormLabel>{translate('Quality')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="quality"
value={qualityId}
values={qualityOptions}
onChange={this.onQualityChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Proper')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="proper"
value={proper}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Real')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="real"
value={real}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Languages')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="languageIds"
value={languageIds}
values={languageOptions}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('IndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Edition')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="edition"
value={edition}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
onChange={this.onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onSaveInputs}
>
{translate('Save')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
FileEditModalContent.propTypes = {
qualityId: PropTypes.number.isRequired,
proper: PropTypes.bool.isRequired,
real: PropTypes.bool.isRequired,
relativePath: PropTypes.string.isRequired,
edition: PropTypes.string.isRequired,
releaseGroup: PropTypes.string.isRequired,
languageIds: PropTypes.arrayOf(PropTypes.number).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerFlags: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveInputs: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default FileEditModalContent;
@@ -0,0 +1,139 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { updateMovieFiles } from 'Store/Actions/movieFileActions';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
import getQualities from 'Utilities/Quality/getQualities';
import FileEditModalContent from './FileEditModalContent';
function createMapStateToProps() {
return createSelector(
createMovieFileSelector(),
(state) => state.settings.qualityProfiles,
(state) => state.settings.languages,
(movieFile, qualityProfiles, languages) => {
const filterItems = ['Any', 'Original'];
const filteredLanguages = languages.items.filter((lang) => !filterItems.includes(lang.name));
const quality = movieFile.quality;
return {
isFetching: qualityProfiles.isSchemaFetching || languages.isFetching,
isPopulated: qualityProfiles.isSchemaPopulated && languages.isPopulated,
error: qualityProfiles.error || languages.error,
qualityId: quality ? quality.quality.id : 0,
real: quality ? quality.revision.real > 0 : false,
proper: quality ? quality.revision.version > 1 : false,
qualities: getQualities(qualityProfiles.schema.items),
languageIds: movieFile.languages ? movieFile.languages.map((l) => l.id) : [],
languages: filteredLanguages,
indexerFlags: movieFile.indexerFlags,
edition: movieFile.edition,
releaseGroup: movieFile.releaseGroup,
relativePath: movieFile.relativePath
};
}
);
}
const mapDispatchToProps = {
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
dispatchUpdateMovieFiles: updateMovieFiles
};
class FileEditModalContentConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
if (!this.props.isPopulated) {
this.props.dispatchFetchQualityProfileSchema();
}
}
//
// Listeners
onSaveInputs = ( payload ) => {
const {
qualityId,
real,
proper,
languageIds,
edition,
releaseGroup,
indexerFlags
} = payload;
const quality = this.props.qualities.find((item) => item.id === qualityId);
const languages = [];
languageIds.forEach((languageId) => {
const language = this.props.languages.find((item) => item.id === parseInt(languageId));
if (language !== undefined) {
languages.push(language);
}
});
const revision = {
version: proper ? 2 : 1,
real: real ? 1 : 0
};
const movieFileIds = [this.props.movieFileId];
this.props.dispatchUpdateMovieFiles({
movieFileIds,
languages,
indexerFlags,
edition,
releaseGroup,
quality: {
quality,
revision
}
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<FileEditModalContent
{...this.props}
onSaveInputs={this.onSaveInputs}
/>
);
}
}
FileEditModalContentConnector.propTypes = {
movieFileId: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
languageIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerFlags: PropTypes.number.isRequired,
qualityId: PropTypes.number.isRequired,
real: PropTypes.bool.isRequired,
edition: PropTypes.string.isRequired,
releaseGroup: PropTypes.string.isRequired,
relativePath: PropTypes.string.isRequired,
proper: PropTypes.bool.isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateMovieFiles: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(FileEditModalContentConnector);
@@ -3,16 +3,14 @@ import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import SelectLanguageModal from 'MovieFile/Language/SelectLanguageModal'; import FileEditModal from 'MovieFile/Edit/FileEditModal';
import MediaInfoConnector from 'MovieFile/MediaInfoConnector'; import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes'; import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
import SelectQualityModal from 'MovieFile/Quality/SelectQualityModal';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import FileDetailsModal from '../FileDetailsModal'; import FileDetailsModal from '../FileDetailsModal';
@@ -28,32 +26,15 @@ class MovieFileEditorRow extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false,
isConfirmDeleteModalOpen: false, isConfirmDeleteModalOpen: false,
isFileDetailsModalOpen: false isFileDetailsModalOpen: false,
isFileEditModalOpen: false
}; };
} }
// //
// Listeners // Listeners
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
onSelectLanguagePress = () => {
this.setState({ isSelectLanguageModalOpen: true });
}
onSelectQualityModalClose = () => {
this.setState({ isSelectQualityModalOpen: false });
}
onSelectLanguageModalClose = () => {
this.setState({ isSelectLanguageModalOpen: false });
}
onDeletePress = () => { onDeletePress = () => {
this.setState({ isConfirmDeleteModalOpen: true }); this.setState({ isConfirmDeleteModalOpen: true });
} }
@@ -76,6 +57,14 @@ class MovieFileEditorRow extends Component {
this.setState({ isFileDetailsModalOpen: false }); this.setState({ isFileDetailsModalOpen: false });
} }
onFileEditPress = () => {
this.setState({ isFileEditModalOpen: true });
}
onFileEditModalClose = () => {
this.setState({ isFileEditModalOpen: false });
}
// //
// Render // Render
@@ -92,9 +81,8 @@ class MovieFileEditorRow extends Component {
} = this.props; } = this.props;
const { const {
isSelectQualityModalOpen,
isSelectLanguageModalOpen,
isFileDetailsModalOpen, isFileDetailsModalOpen,
isFileEditModalOpen,
isConfirmDeleteModalOpen isConfirmDeleteModalOpen
} = this.state; } = this.state;
@@ -132,10 +120,8 @@ class MovieFileEditorRow extends Component {
{formatBytes(size)} {formatBytes(size)}
</TableRowCell> </TableRowCell>
<TableRowCellButton <TableRowCell
className={styles.language} className={styles.language}
title={translate('ClickToChangeLanguage')}
onPress={this.onSelectLanguagePress}
> >
{ {
showLanguagePlaceholder && showLanguagePlaceholder &&
@@ -149,12 +135,10 @@ class MovieFileEditorRow extends Component {
languages={languages} languages={languages}
/> />
} }
</TableRowCellButton> </TableRowCell>
<TableRowCellButton <TableRowCell
className={styles.quality} className={styles.quality}
title={translate('ClickToChangeQuality')}
onPress={this.onSelectQualityPress}
> >
{ {
showQualityPlaceholder && showQualityPlaceholder &&
@@ -169,7 +153,7 @@ class MovieFileEditorRow extends Component {
isCutoffNotMet={qualityCutoffNotMet} isCutoffNotMet={qualityCutoffNotMet}
/> />
} }
</TableRowCellButton> </TableRowCell>
<TableRowCell <TableRowCell
className={styles.formats} className={styles.formats}
@@ -180,6 +164,11 @@ class MovieFileEditorRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.actions}> <TableRowCell className={styles.actions}>
<IconButton
name={icons.EDIT}
onPress={this.onFileEditPress}
/>
<IconButton <IconButton
name={icons.MEDIA_INFO} name={icons.MEDIA_INFO}
onPress={this.onFileDetailsPress} onPress={this.onFileDetailsPress}
@@ -198,6 +187,12 @@ class MovieFileEditorRow extends Component {
mediaInfo={mediaInfo} mediaInfo={mediaInfo}
/> />
<FileEditModal
movieFileId={id}
isOpen={isFileEditModalOpen}
onModalClose={this.onFileEditModalClose}
/>
<ConfirmModal <ConfirmModal
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
ids={[id]} ids={[id]}
@@ -208,22 +203,6 @@ class MovieFileEditorRow extends Component {
onConfirm={this.onConfirmDelete} onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}
/> />
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
ids={[id]}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
<SelectLanguageModal
isOpen={isSelectLanguageModalOpen}
ids={[id]}
languageIds={languages ? languages.map((l) => l.id) : []}
onModalClose={this.onSelectLanguageModalClose}
/>
</TableRow> </TableRow>
); );
} }
@@ -49,6 +49,12 @@ function EditImportListModalContent(props) {
fields fields
} = item; } = item;
const importListTypeOptions = [
{ key: 'manual', value: 'Manual' },
{ key: 'automatic', value: 'Automatic Add' },
{ key: 'exclusion', value: 'Exclusion List' }
];
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
@@ -100,8 +106,9 @@ function EditImportListModalContent(props) {
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel> <FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.SELECT}
name="enableAuto" name="enableAuto"
values={importListTypeOptions}
helpText={translate('EnableAutoHelpText')} helpText={translate('EnableAutoHelpText')}
{...enableAuto} {...enableAuto}
onChange={onInputChange} onChange={onInputChange}
@@ -173,7 +180,7 @@ function EditImportListModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
helpText={translate('TagsHelpText')} helpText={translate('ListTagsHelpText')}
{...tags} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />
@@ -1,10 +1,12 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'lodash';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import getProviderState from 'Utilities/State/getProviderState'; import getProviderState from 'Utilities/State/getProviderState';
import { set, updateItem } from '../baseActions'; import { set, updateItem } from '../baseActions';
const abortCurrentRequests = {}; const abortCurrentRequests = {};
let lastSaveData = null;
export function createCancelSaveProviderHandler(section) { export function createCancelSaveProviderHandler(section) {
return function(getState, payload, dispatch) { return function(getState, payload, dispatch) {
@@ -26,25 +28,33 @@ function createSaveProviderHandler(section, url, options = {}) {
} = payload; } = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, section); const saveData = getProviderState({ id, ...otherPayload }, getState, section);
const requestUrl = id ? `${url}/${id}` : url;
const params = { ...queryParams };
// If the user is re-saving the same provider without changes
// force it to be saved. Only applies to editing existing providers.
if (id && _.isEqual(saveData, lastSaveData)) {
params.forceSave = true;
}
lastSaveData = saveData;
const ajaxOptions = { const ajaxOptions = {
url: `${url}?${$.param(queryParams, true)}`, url: `${requestUrl}?${$.param(params, true)}`,
method: 'POST', method: id ? 'PUT' : 'POST',
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
data: JSON.stringify(saveData) data: JSON.stringify(saveData)
}; };
if (id) {
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
ajaxOptions.method = 'PUT';
}
const { request, abortRequest } = createAjaxRequest(ajaxOptions); const { request, abortRequest } = createAjaxRequest(ajaxOptions);
abortCurrentRequests[section] = abortRequest; abortCurrentRequests[section] = abortRequest;
request.done((data) => { request.done((data) => {
lastSaveData = null;
dispatch(batchActions([ dispatch(batchActions([
updateItem({ section, ...data }), updateItem({ section, ...data }),
@@ -0,0 +1,48 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerFlags';
//
// Actions Types
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
//
// Action Creators
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
},
//
// Reducers
reducers: {
}
};
+55 -3
View File
@@ -1,8 +1,11 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import { set, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import createRemoveItemHandler from './Creators/createRemoveItemHandler'; import createRemoveItemHandler from './Creators/createRemoveItemHandler';
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
@@ -25,6 +28,7 @@ export const defaultState = {
sortDirection: sortDirections.DESCENDING, sortDirection: sortDirections.DESCENDING,
error: null, error: null,
items: [], items: [],
isRemoving: false,
columns: [ columns: [
{ {
@@ -96,7 +100,8 @@ export const GOTO_LAST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistLastPage';
export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage'; export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage';
export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort'; export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort';
export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption'; export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption';
export const REMOVE_FROM_BLACKLIST = 'blacklist/removeFromBlacklist'; export const REMOVE_BLACKLIST_ITEM = 'blacklist/removeBlacklistItem';
export const REMOVE_BLACKLIST_ITEMS = 'blacklist/removeBlacklistItems';
export const CLEAR_BLACKLIST = 'blacklist/clearBlacklist'; export const CLEAR_BLACKLIST = 'blacklist/clearBlacklist';
// //
@@ -110,7 +115,8 @@ export const gotoBlacklistLastPage = createThunk(GOTO_LAST_BLACKLIST_PAGE);
export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE); export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE);
export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT); export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT);
export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION); export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION);
export const removeFromBlacklist = createThunk(REMOVE_FROM_BLACKLIST); export const removeBlacklistItem = createThunk(REMOVE_BLACKLIST_ITEM);
export const removeBlacklistItems = createThunk(REMOVE_BLACKLIST_ITEMS);
export const clearBlacklist = createAction(CLEAR_BLACKLIST); export const clearBlacklist = createAction(CLEAR_BLACKLIST);
// //
@@ -131,7 +137,53 @@ export const actionHandlers = handleThunks({
[serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT [serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT
}), }),
[REMOVE_FROM_BLACKLIST]: createRemoveItemHandler(section, '/blacklist') [REMOVE_BLACKLIST_ITEM]: createRemoveItemHandler(section, '/blacklist'),
[REMOVE_BLACKLIST_ITEMS]: function(getState, payload, dispatch) {
const {
ids
} = payload;
dispatch(batchActions([
...ids.map((id) => {
return updateItem({
section,
id,
isRemoving: true
});
}),
set({ section, isRemoving: true })
]));
const promise = createAjaxRequest({
url: '/blacklist/bulk',
method: 'DELETE',
dataType: 'json',
data: JSON.stringify({ ids })
}).request;
promise.done((data) => {
// Don't use batchActions with thunks
dispatch(fetchBlacklist());
dispatch(set({ section, isRemoving: false }));
});
promise.fail((xhr) => {
dispatch(batchActions([
...ids.map((id) => {
return updateItem({
section,
id,
isRemoving: false
});
}),
set({ section, isRemoving: false })
]));
});
}
}); });
// //
+28 -1
View File
@@ -141,7 +141,10 @@ export const actionHandlers = handleThunks({
const { const {
movieFileIds, movieFileIds,
languages, languages,
quality indexerFlags,
quality,
edition,
releaseGroup
} = payload; } = payload;
dispatch(set({ section, isSaving: true })); dispatch(set({ section, isSaving: true }));
@@ -154,10 +157,22 @@ export const actionHandlers = handleThunks({
data.languages = languages; data.languages = languages;
} }
if (indexerFlags !== undefined) {
data.indexerFlags = indexerFlags;
}
if (quality) { if (quality) {
data.quality = quality; data.quality = quality;
} }
if (releaseGroup) {
data.releaseGroup = releaseGroup;
}
if (edition) {
data.edition = edition;
}
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/movieFile/editor', url: '/movieFile/editor',
method: 'PUT', method: 'PUT',
@@ -174,10 +189,22 @@ export const actionHandlers = handleThunks({
props.languages = languages; props.languages = languages;
} }
if (indexerFlags) {
props.indexerFlags = indexerFlags;
}
if (quality) { if (quality) {
props.quality = quality; props.quality = quality;
} }
if (edition) {
props.edition = edition;
}
if (releaseGroup) {
props.releaseGroup = releaseGroup;
}
return updateItem({ section, id, ...props }); return updateItem({ section, id, ...props });
}), }),
+6 -6
View File
@@ -314,9 +314,9 @@ export const actionHandlers = handleThunks({
}).request; }).request;
promise.done((data) => { promise.done((data) => {
dispatch(batchActions([ dispatch(fetchQueue());
fetchQueue(),
dispatch(batchActions([
...ids.map((id) => { ...ids.map((id) => {
return updateItem({ return updateItem({
section: paged, section: paged,
@@ -400,10 +400,10 @@ export const actionHandlers = handleThunks({
}).request; }).request;
promise.done((data) => { promise.done((data) => {
dispatch(batchActions([ // Don't use batchActions with thunks
set({ section: paged, isRemoving: false }), dispatch(fetchQueue());
fetchQueue()
])); dispatch(set({ section: paged, isRemoving: false }));
}); });
promise.fail((xhr) => { promise.fail((xhr) => {
@@ -10,6 +10,7 @@ import general from './Settings/general';
import importExclusions from './Settings/importExclusions'; import importExclusions from './Settings/importExclusions';
import importListOptions from './Settings/importListOptions'; import importListOptions from './Settings/importListOptions';
import importLists from './Settings/importLists'; import importLists from './Settings/importLists';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions'; import indexerOptions from './Settings/indexerOptions';
import indexers from './Settings/indexers'; import indexers from './Settings/indexers';
import languages from './Settings/languages'; import languages from './Settings/languages';
@@ -31,6 +32,7 @@ export * from './Settings/delayProfiles';
export * from './Settings/downloadClients'; export * from './Settings/downloadClients';
export * from './Settings/downloadClientOptions'; export * from './Settings/downloadClientOptions';
export * from './Settings/general'; export * from './Settings/general';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions'; export * from './Settings/indexerOptions';
export * from './Settings/indexers'; export * from './Settings/indexers';
export * from './Settings/languages'; export * from './Settings/languages';
@@ -66,6 +68,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState, downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState, downloadClientOptions: downloadClientOptions.defaultState,
general: general.defaultState, general: general.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState, indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState, indexers: indexers.defaultState,
languages: languages.defaultState, languages: languages.defaultState,
@@ -109,6 +112,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers, ...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers, ...downloadClientOptions.actionHandlers,
...general.actionHandlers, ...general.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers, ...indexerOptions.actionHandlers,
...indexers.actionHandlers, ...indexers.actionHandlers,
...languages.actionHandlers, ...languages.actionHandlers,
@@ -143,6 +147,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers, ...downloadClients.reducers,
...downloadClientOptions.reducers, ...downloadClientOptions.reducers,
...general.reducers, ...general.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers, ...indexerOptions.reducers,
...indexers.reducers, ...indexers.reducers,
...languages.reducers, ...languages.reducers,
@@ -19,6 +19,7 @@ function getInternalLink(source) {
case 'IndexerRssCheck': case 'IndexerRssCheck':
case 'IndexerSearchCheck': case 'IndexerSearchCheck':
case 'IndexerStatusCheck': case 'IndexerStatusCheck':
case 'IndexerLongTermStatusCheck':
return ( return (
<IconButton <IconButton
name={icons.SETTINGS} name={icons.SETTINGS}
@@ -4,6 +4,7 @@ import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday'; import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow'; import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday'; import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) { function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
if (!date) { if (!date) {
@@ -21,15 +22,15 @@ function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat,
} }
if (isYesterday(date)) { if (isYesterday(date)) {
return 'Yesterday'; return translate('Yesterday');
} }
if (isTodayDate) { if (isTodayDate) {
return 'Today'; return translate('Today');
} }
if (isTomorrow(date)) { if (isTomorrow(date)) {
return 'Tomorrow'; return translate('Tomorrow');
} }
if (isInNextWeek(date)) { if (isInNextWeek(date)) {
+4 -3
View File
@@ -1,11 +1,12 @@
import moment from 'moment';
function isToday(date) { function isToday(date) {
if (!date) { if (!date) {
return false; return false;
} }
return moment(date).isSame(moment(), 'day'); const dateObj = (typeof date === 'object') ? date : new Date(date);
const today = new Date();
return dateObj.getDate() === today.getDate() && dateObj.getMonth() === today.getMonth() && dateObj.getFullYear() === today.getFullYear();
} }
export default isToday; export default isToday;
+5 -3
View File
@@ -1,11 +1,13 @@
import moment from 'moment';
function isTomorrow(date) { function isTomorrow(date) {
if (!date) { if (!date) {
return false; return false;
} }
return moment(date).isSame(moment().add(1, 'day'), 'day'); const dateObj = (typeof date === 'object') ? date : new Date(date);
const today = new Date();
const tomorrow = new Date((today.setDate(today.getDate() + 1)));
return dateObj.getDate() === tomorrow.getDate() && dateObj.getMonth() === tomorrow.getMonth() && dateObj.getFullYear() === tomorrow.getFullYear();
} }
export default isTomorrow; export default isTomorrow;
+5 -3
View File
@@ -1,11 +1,13 @@
import moment from 'moment';
function isYesterday(date) { function isYesterday(date) {
if (!date) { if (!date) {
return false; return false;
} }
return moment(date).isSame(moment().subtract(1, 'day'), 'day'); const dateObj = (typeof date === 'object') ? date : new Date(date);
const today = new Date();
const yesterday = new Date((today.setDate(today.getDate() - 1)));
return dateObj.getDate() === yesterday.getDate() && dateObj.getMonth() === yesterday.getMonth() && dateObj.getFullYear() === yesterday.getFullYear();
} }
export default isYesterday; export default isYesterday;
+7 -2
View File
@@ -37,13 +37,14 @@ Compression=lzma2/normal
AppContact={#ForumsURL} AppContact={#ForumsURL}
VersionInfoVersion={#BaseVersion}.{#BuildNumber} VersionInfoVersion={#BaseVersion}.{#BuildNumber}
SetupLogging=yes SetupLogging=yes
OutputDir=output
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}" Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; GroupDescription: "Start automatically"; Flags: exclusive Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
@@ -57,6 +58,9 @@ Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
Name: "{userstartup}\{#AppName}"; Filename: "{app}\Radarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut Name: "{userstartup}\{#AppName}"; Filename: "{app}\Radarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut
[InstallDelete]
Name: "{app}"; Type: filesandordirs
[Run] [Run]
Filename: "{app}\Radarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated; Filename: "{app}\Radarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated;
Filename: "{app}\Radarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none; Filename: "{app}\Radarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
@@ -72,5 +76,6 @@ function PrepareToInstall(var NeedsRestart: Boolean): String;
var var
ResultCode: Integer; ResultCode: Integer;
begin begin
Exec(ExpandConstant('{commonappdata}\Radarr\bin\Radarr.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode) Exec('net', 'stop radarr', '', 0, ewWaitUntilTerminated, ResultCode)
Exec('sc', 'delete radarr', '', 0, ewWaitUntilTerminated, ResultCode)
end; end;
@@ -21,7 +21,7 @@ namespace NzbDrone.Api.ImportList
base.MapToResource(resource, definition); base.MapToResource(resource, definition);
resource.Enabled = definition.Enabled; resource.Enabled = definition.Enabled;
resource.EnableAuto = definition.EnableAuto; resource.EnableAuto = (int)definition.EnableAuto;
resource.ProfileId = definition.ProfileId; resource.ProfileId = definition.ProfileId;
resource.RootFolderPath = definition.RootFolderPath; resource.RootFolderPath = definition.RootFolderPath;
resource.ShouldMonitor = definition.ShouldMonitor; resource.ShouldMonitor = definition.ShouldMonitor;
@@ -34,7 +34,7 @@ namespace NzbDrone.Api.ImportList
base.MapToModel(definition, resource); base.MapToModel(definition, resource);
definition.Enabled = resource.Enabled; definition.Enabled = resource.Enabled;
definition.EnableAuto = resource.EnableAuto; definition.EnableAuto = (ImportListType)resource.EnableAuto;
definition.ProfileId = resource.ProfileId; definition.ProfileId = resource.ProfileId;
definition.RootFolderPath = resource.RootFolderPath; definition.RootFolderPath = resource.RootFolderPath;
definition.ShouldMonitor = resource.ShouldMonitor; definition.ShouldMonitor = resource.ShouldMonitor;
@@ -6,7 +6,7 @@ namespace NzbDrone.Api.ImportList
public class ImportListResource : ProviderResource public class ImportListResource : ProviderResource
{ {
public bool Enabled { get; set; } public bool Enabled { get; set; }
public bool EnableAuto { get; set; } public int EnableAuto { get; set; }
public bool ShouldMonitor { get; set; } public bool ShouldMonitor { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }
public int ProfileId { get; set; } public int ProfileId { get; set; }
@@ -42,6 +42,8 @@ namespace NzbDrone.Automation.Test
// Timeout as windows automation tests seem to take alot longer to get going // Timeout as windows automation tests seem to take alot longer to get going
driver = new ChromeDriver(service, options, new TimeSpan(0, 3, 0)); driver = new ChromeDriver(service, options, new TimeSpan(0, 3, 0));
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger()); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
_runner.KillAll(); _runner.KillAll();
_runner.Start(); _runner.Start();
@@ -62,6 +64,19 @@ namespace NzbDrone.Automation.Test
.Select(e => e.Text); .Select(e => e.Text);
} }
protected void TakeScreenshot(string name)
{
try
{
Screenshot image = ((ITakesScreenshot)driver).GetScreenshot();
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to save screenshot {name}, {ex.Message}");
}
}
[OneTimeTearDown] [OneTimeTearDown]
public void SmokeTestTearDown() public void SmokeTestTearDown()
{ {
+18 -3
View File
@@ -1,4 +1,5 @@
using FluentAssertions; using System.Reflection;
using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Automation.Test.PageModel; using NzbDrone.Automation.Test.PageModel;
using OpenQA.Selenium; using OpenQA.Selenium;
@@ -21,6 +22,10 @@ namespace NzbDrone.Automation.Test
{ {
_page.MovieNavIcon.Click(); _page.MovieNavIcon.Click();
_page.WaitForNoSpinner(); _page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("div[class*='MovieIndex']")).Should().NotBeNull(); _page.Find(By.CssSelector("div[class*='MovieIndex']")).Should().NotBeNull();
} }
@@ -30,6 +35,9 @@ namespace NzbDrone.Automation.Test
_page.CalendarNavIcon.Click(); _page.CalendarNavIcon.Click();
_page.WaitForNoSpinner(); _page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("div[class*='CalendarPage']")).Should().NotBeNull(); _page.Find(By.CssSelector("div[class*='CalendarPage']")).Should().NotBeNull();
} }
@@ -39,6 +47,9 @@ namespace NzbDrone.Automation.Test
_page.ActivityNavIcon.Click(); _page.ActivityNavIcon.Click();
_page.WaitForNoSpinner(); _page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.LinkText("Queue")).Should().NotBeNull(); _page.Find(By.LinkText("Queue")).Should().NotBeNull();
_page.Find(By.LinkText("History")).Should().NotBeNull(); _page.Find(By.LinkText("History")).Should().NotBeNull();
_page.Find(By.LinkText("Blacklist")).Should().NotBeNull(); _page.Find(By.LinkText("Blacklist")).Should().NotBeNull();
@@ -50,6 +61,9 @@ namespace NzbDrone.Automation.Test
_page.SystemNavIcon.Click(); _page.SystemNavIcon.Click();
_page.WaitForNoSpinner(); _page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("div[class*='Health']")).Should().NotBeNull(); _page.Find(By.CssSelector("div[class*='Health']")).Should().NotBeNull();
} }
@@ -58,11 +72,12 @@ namespace NzbDrone.Automation.Test
{ {
_page.MovieNavIcon.Click(); _page.MovieNavIcon.Click();
_page.WaitForNoSpinner(); _page.WaitForNoSpinner();
_page.Find(By.LinkText("Add New")).Click(); _page.Find(By.LinkText("Add New")).Click();
_page.WaitForNoSpinner(); _page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("input[class*='AddNewMovie-searchInput']")).Should().NotBeNull(); _page.Find(By.CssSelector("input[class*='AddNewMovie-searchInput']")).Should().NotBeNull();
} }
} }
@@ -8,7 +8,7 @@
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="NunitXml.TestLogger" Version="2.1.41" /> <PackageReference Include="NunitXml.TestLogger" Version="2.1.41" />
<PackageReference Include="Selenium.Support" Version="3.141.0" /> <PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="84.0.4147.3001" /> <PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="86.0.4240.2200" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />
@@ -731,7 +731,7 @@ namespace NzbDrone.Common.Test.DiskTests
// Note: never returns anything. // Note: never returns anything.
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileInfos(It.IsAny<string>())) .Setup(v => v.GetFileInfos(It.IsAny<string>(), SearchOption.TopDirectoryOnly))
.Returns(new List<FileInfo>()); .Returns(new List<FileInfo>());
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
@@ -765,8 +765,8 @@ namespace NzbDrone.Common.Test.DiskTests
.Returns<string>(v => new DirectoryInfo(v).GetDirectories().ToList()); .Returns<string>(v => new DirectoryInfo(v).GetDirectories().ToList());
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileInfos(It.IsAny<string>())) .Setup(v => v.GetFileInfos(It.IsAny<string>(), SearchOption.TopDirectoryOnly))
.Returns<string>(v => new DirectoryInfo(v).GetFiles().ToList()); .Returns<string, SearchOption>((v, _) => new DirectoryInfo(v).GetFiles().ToList());
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileSize(It.IsAny<string>())) .Setup(v => v.GetFileSize(It.IsAny<string>()))
@@ -8,6 +8,8 @@ namespace NzbDrone.Common.Test.Http
public class HttpUriFixture : TestBase public class HttpUriFixture : TestBase
{ {
[TestCase("abc://my_host.com:8080/root/api/")] [TestCase("abc://my_host.com:8080/root/api/")]
[TestCase("abc://my_host.com:8080//root/api/")]
[TestCase("abc://my_host.com:8080/root//api/")]
public void should_parse(string uri) public void should_parse(string uri)
{ {
var newUri = new HttpUri(uri); var newUri = new HttpUri(uri);
+9 -2
View File
@@ -505,13 +505,20 @@ namespace NzbDrone.Common.Disk
return di.GetDirectories().ToList(); return di.GetDirectories().ToList();
} }
public List<FileInfo> GetFileInfos(string path) public FileInfo GetFileInfo(string path)
{
Ensure.That(path, () => path).IsValidPath();
return new FileInfo(path);
}
public List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{ {
Ensure.That(path, () => path).IsValidPath(); Ensure.That(path, () => path).IsValidPath();
var di = new DirectoryInfo(path); var di = new DirectoryInfo(path);
return di.GetFiles().ToList(); return di.GetFiles("*", searchOption).ToList();
} }
public void RemoveEmptySubfolders(string path) public void RemoveEmptySubfolders(string path)
+2 -1
View File
@@ -52,7 +52,8 @@ namespace NzbDrone.Common.Disk
List<IMount> GetMounts(); List<IMount> GetMounts();
IMount GetMount(string path); IMount GetMount(string path);
List<DirectoryInfo> GetDirectoryInfos(string path); List<DirectoryInfo> GetDirectoryInfos(string path);
List<FileInfo> GetFileInfos(string path); FileInfo GetFileInfo(string path);
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
void RemoveEmptySubfolders(string path); void RemoveEmptySubfolders(string path);
void SaveStream(Stream stream, string path); void SaveStream(Stream stream, string path);
bool IsValidFilePermissionMask(string mask); bool IsValidFilePermissionMask(string mask);
+1 -1
View File
@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
{ {
public class HttpUri : IEquatable<HttpUri> public class HttpUri : IEquatable<HttpUri>
{ {
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/)[^/?#\r\n]+)+/?|/)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly string _uri; private readonly string _uri;
public string FullUri => _uri; public string FullUri => _uri;
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class IndexerLongTermStatusCheckFixture : CoreTest<IndexerLongTermStatusCheck>
{
private List<IIndexer> _indexers = new List<IIndexer>();
private List<IndexerStatus> _blockedIndexers = new List<IndexerStatus>();
[SetUp]
public void SetUp()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.GetAvailableProviders())
.Returns(_indexers);
Mocker.GetMock<IIndexerStatusService>()
.Setup(v => v.GetBlockedProviders())
.Returns(_blockedIndexers);
Mocker.GetMock<ILocalizationService>()
.Setup(v => v.GetLocalizedString(It.IsAny<string>()))
.Returns("Error");
}
private Mock<IIndexer> GivenIndexer(int id, double backoffHours, double failureHours)
{
var mockIndexer = new Mock<IIndexer>();
mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = id });
mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true);
_indexers.Add(mockIndexer.Object);
if (backoffHours != 0.0)
{
_blockedIndexers.Add(new IndexerStatus
{
ProviderId = id,
InitialFailure = DateTime.UtcNow.AddHours(-failureHours),
MostRecentFailure = DateTime.UtcNow.AddHours(-0.1),
EscalationLevel = 5,
DisabledTill = DateTime.UtcNow.AddHours(backoffHours)
});
}
return mockIndexer;
}
[Test]
public void should_not_return_error_when_no_indexers()
{
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_warning_if_indexer_unavailable()
{
GivenIndexer(1, 10.0, 24.0);
GivenIndexer(2, 0.0, 0.0);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_error_if_all_indexers_unavailable()
{
GivenIndexer(1, 10.0, 24.0);
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_warning_if_few_indexers_unavailable()
{
GivenIndexer(1, 10.0, 24.0);
GivenIndexer(2, 10.0, 24.0);
GivenIndexer(3, 0.0, 0.0);
Subject.Check().ShouldBeWarning();
}
}
}
@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test] [Test]
public void should_return_warning_if_indexer_unavailable() public void should_return_warning_if_indexer_unavailable()
{ {
GivenIndexer(1, 10.0, 24.0); GivenIndexer(1, 2.0, 4.0);
GivenIndexer(2, 0.0, 0.0); GivenIndexer(2, 0.0, 0.0);
Subject.Check().ShouldBeWarning(); Subject.Check().ShouldBeWarning();
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test] [Test]
public void should_return_error_if_all_indexers_unavailable() public void should_return_error_if_all_indexers_unavailable()
{ {
GivenIndexer(1, 10.0, 24.0); GivenIndexer(1, 2.0, 4.0);
Subject.Check().ShouldBeError(); Subject.Check().ShouldBeError();
} }
@@ -82,8 +82,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test] [Test]
public void should_return_warning_if_few_indexers_unavailable() public void should_return_warning_if_few_indexers_unavailable()
{ {
GivenIndexer(1, 10.0, 24.0); GivenIndexer(1, 2.0, 4.0);
GivenIndexer(2, 10.0, 24.0); GivenIndexer(2, 2.0, 4.0);
GivenIndexer(3, 0.0, 0.0); GivenIndexer(3, 0.0, 0.0);
Subject.Check().ShouldBeWarning(); Subject.Check().ShouldBeWarning();
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
var mockList = new Mock<IImportList>(); var mockList = new Mock<IImportList>();
mockList.SetupGet(s => s.Definition).Returns(new ImportListDefinition { Id = id }); mockList.SetupGet(s => s.Definition).Returns(new ImportListDefinition { Id = id });
mockList.SetupGet(s => s.EnableAuto).Returns(true); mockList.SetupGet(s => s.EnableAuto).Returns(ImportListType.Automatic);
_lists.Add(mockList.Object); _lists.Add(mockList.Object);
@@ -42,12 +42,12 @@ namespace NzbDrone.Core.Test.ImportListTests
.Returns<Movie>(m => new Movie { TmdbId = m.TmdbId }); .Returns<Movie>(m => new Movie { TmdbId = m.TmdbId });
} }
private void GivenList(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult) private void GivenList(int id, bool enabled, ImportListType enabledAuto, ImportListFetchResult fetchResult)
{ {
CreateListResult(id, enabled, enabledAuto, fetchResult); CreateListResult(id, enabled, enabledAuto, fetchResult);
} }
private Mock<IImportList> CreateListResult(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult) private Mock<IImportList> CreateListResult(int id, bool enabled, ImportListType enabledAuto, ImportListFetchResult fetchResult)
{ {
var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto }; var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto };
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.ImportListTests
public void should_return_failure_if_blocked_list() public void should_return_failure_if_blocked_list()
{ {
var fetchResult = new ImportListFetchResult(); var fetchResult = new ImportListFetchResult();
GivenList(1, true, true, fetchResult); GivenList(1, true, ImportListType.Automatic, fetchResult);
GivenBlockedList(1); GivenBlockedList(1);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
@@ -82,11 +82,11 @@ namespace NzbDrone.Core.Test.ImportListTests
public void should_return_failure_if_one_blocked_list_one_good_list() public void should_return_failure_if_one_blocked_list_one_good_list()
{ {
var fetchResult1 = new ImportListFetchResult(); var fetchResult1 = new ImportListFetchResult();
GivenList(1, true, true, fetchResult1); GivenList(1, true, ImportListType.Automatic, fetchResult1);
GivenBlockedList(1); GivenBlockedList(1);
var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true }; var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true };
GivenList(2, true, true, fetchResult2); GivenList(2, true, ImportListType.Automatic, fetchResult2);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeTrue(); listResult.AnyFailure.Should().BeTrue();
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.ImportListTests
public void should_return_failure_if_single_list_fails() public void should_return_failure_if_single_list_fails()
{ {
var fetchResult = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true }; var fetchResult = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true };
GivenList(1, true, true, fetchResult); GivenList(1, true, ImportListType.Automatic, fetchResult);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeTrue(); listResult.AnyFailure.Should().BeTrue();
@@ -106,9 +106,9 @@ namespace NzbDrone.Core.Test.ImportListTests
public void should_return_failure_if_any_list_fails() public void should_return_failure_if_any_list_fails()
{ {
var fetchResult1 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true }; var fetchResult1 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true };
GivenList(1, true, true, fetchResult1); GivenList(1, true, ImportListType.Automatic, fetchResult1);
var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false }; var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false };
GivenList(2, true, true, fetchResult2); GivenList(2, true, ImportListType.Automatic, fetchResult2);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeTrue(); listResult.AnyFailure.Should().BeTrue();
@@ -131,7 +131,7 @@ namespace NzbDrone.Core.Test.ImportListTests
{ {
var listId = 1; var listId = 1;
var fetchResult = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false }; var fetchResult = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false };
GivenList(listId, true, true, fetchResult); GivenList(listId, true, ImportListType.Automatic, fetchResult);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeFalse(); listResult.AnyFailure.Should().BeFalse();
@@ -145,7 +145,7 @@ namespace NzbDrone.Core.Test.ImportListTests
{ {
var listId = 1; var listId = 1;
var fetchResult = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true }; var fetchResult = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true };
GivenList(listId, true, true, fetchResult); GivenList(listId, true, ImportListType.Automatic, fetchResult);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeTrue(); listResult.AnyFailure.Should().BeTrue();
@@ -159,10 +159,10 @@ namespace NzbDrone.Core.Test.ImportListTests
{ {
var passedListId = 1; var passedListId = 1;
var fetchResult1 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false }; var fetchResult1 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false };
GivenList(passedListId, true, true, fetchResult1); GivenList(passedListId, true, ImportListType.Automatic, fetchResult1);
var failedListId = 2; var failedListId = 2;
var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true }; var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = true };
GivenList(failedListId, true, true, fetchResult2); GivenList(failedListId, true, ImportListType.Automatic, fetchResult2);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeTrue(); listResult.AnyFailure.Should().BeTrue();
@@ -176,10 +176,10 @@ namespace NzbDrone.Core.Test.ImportListTests
{ {
var passedListId = 1; var passedListId = 1;
var fetchResult1 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false }; var fetchResult1 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false };
GivenList(passedListId, true, true, fetchResult1); GivenList(passedListId, true, ImportListType.Automatic, fetchResult1);
var failedListId = 2; var failedListId = 2;
var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false }; var fetchResult2 = new ImportListFetchResult { Movies = _listMovies, AnyFailure = false };
GivenList(failedListId, true, true, fetchResult2); GivenList(failedListId, true, ImportListType.Automatic, fetchResult2);
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeFalse(); listResult.AnyFailure.Should().BeFalse();
@@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.ImportList
.Returns(cleanLevel); .Returns(cleanLevel);
} }
private void GivenList(int id, bool enabledAuto) private void GivenList(int id, ImportListType enabledAuto)
{ {
var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto }; var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto };
@@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.ImportList
CreateListResult(id, enabledAuto); CreateListResult(id, enabledAuto);
} }
private Mock<IImportList> CreateListResult(int id, bool enabledAuto) private Mock<IImportList> CreateListResult(int id, ImportListType enabledAuto)
{ {
var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto }; var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto };
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ImportList
public void should_not_clean_library_if_config_value_disable() public void should_not_clean_library_if_config_value_disable()
{ {
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
Subject.Execute(_commandAll); Subject.Execute(_commandAll);
@@ -149,7 +149,7 @@ namespace NzbDrone.Core.Test.ImportList
public void should_log_only_on_clean_library_if_config_value_logonly() public void should_log_only_on_clean_library_if_config_value_logonly()
{ {
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("logOnly"); GivenCleanLevel("logOnly");
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
@@ -172,7 +172,7 @@ namespace NzbDrone.Core.Test.ImportList
public void should_unmonitor_on_clean_library_if_config_value_keepAndUnmonitor() public void should_unmonitor_on_clean_library_if_config_value_keepAndUnmonitor()
{ {
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("keepAndUnmonitor"); GivenCleanLevel("keepAndUnmonitor");
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
@@ -197,7 +197,7 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
_importListFetch.Movies[0].TmdbId = 6; _importListFetch.Movies[0].TmdbId = 6;
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("keepAndUnmonitor"); GivenCleanLevel("keepAndUnmonitor");
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
@@ -217,7 +217,7 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies[0].TmdbId = 0; _importListFetch.Movies[0].TmdbId = 0;
_importListFetch.Movies[0].ImdbId = "6"; _importListFetch.Movies[0].ImdbId = "6";
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("keepAndUnmonitor"); GivenCleanLevel("keepAndUnmonitor");
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
@@ -234,7 +234,7 @@ namespace NzbDrone.Core.Test.ImportList
public void should_delete_movies_not_files_on_clean_library_if_config_value_logonly() public void should_delete_movies_not_files_on_clean_library_if_config_value_logonly()
{ {
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("removeAndKeep"); GivenCleanLevel("removeAndKeep");
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
@@ -260,7 +260,7 @@ namespace NzbDrone.Core.Test.ImportList
public void should_delete_movies_and_files_on_clean_library_if_config_value_logonly() public void should_delete_movies_and_files_on_clean_library_if_config_value_logonly()
{ {
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("removeAndDelete"); GivenCleanLevel("removeAndDelete");
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
@@ -288,7 +288,7 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
GivenListFailure(); GivenListFailure();
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
Subject.Execute(_commandAll); Subject.Execute(_commandAll);
@@ -301,7 +301,7 @@ namespace NzbDrone.Core.Test.ImportList
public void should_add_new_movies_from_single_list_to_library() public void should_add_new_movies_from_single_list_to_library()
{ {
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
Subject.Execute(_commandAll); Subject.Execute(_commandAll);
@@ -317,8 +317,8 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
_importListFetch.Movies.AddRange(_list2Movies); _importListFetch.Movies.AddRange(_list2Movies);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenList(2, true); GivenList(2, ImportListType.Automatic);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
@@ -335,8 +335,8 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
_importListFetch.Movies.AddRange(_list2Movies); _importListFetch.Movies.AddRange(_list2Movies);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenList(2, false); GivenList(2, ImportListType.Manual);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
@@ -354,8 +354,8 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies.AddRange(_list2Movies); _importListFetch.Movies.AddRange(_list2Movies);
_importListFetch.Movies[0].TmdbId = 4; _importListFetch.Movies[0].TmdbId = 4;
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenList(2, true); GivenList(2, ImportListType.Automatic);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
@@ -372,8 +372,8 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
_importListFetch.Movies.AddRange(_list2Movies); _importListFetch.Movies.AddRange(_list2Movies);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenList(2, true); GivenList(2, ImportListType.Automatic);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
@@ -394,8 +394,8 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch.Movies.ForEach(m => m.ListId = 1); _importListFetch.Movies.ForEach(m => m.ListId = 1);
_importListFetch.Movies.AddRange(_list2Movies); _importListFetch.Movies.AddRange(_list2Movies);
GivenList(1, true); GivenList(1, ImportListType.Automatic);
GivenList(2, true); GivenList(2, ImportListType.Automatic);
GivenCleanLevel("disabled"); GivenCleanLevel("disabled");
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
@@ -40,15 +41,16 @@ namespace NzbDrone.Core.Test.MediaCoverTests
new MediaCover.MediaCover { CoverType = MediaCoverTypes.Banner } new MediaCover.MediaCover { CoverType = MediaCoverTypes.Banner }
}; };
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileGetLastWrite(It.IsAny<string>())) var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
.Returns(new DateTime(1234)); var fileInfo = new FileInfo(path);
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(It.IsAny<string>())) Mocker.GetMock<IDiskProvider>()
.Returns(true); .Setup(c => c.GetFileInfo(It.IsAny<string>()))
.Returns(fileInfo);
Subject.ConvertToLocalUrls(12, covers); Subject.ConvertToLocalUrls(12, covers);
covers.Single().Url.Should().Be("/MediaCover/12/banner.jpg?lastWrite=1234"); covers.Single().Url.Should().Be($"/MediaCover/12/banner.jpg?lastWrite={fileInfo.LastWriteTimeUtc.Ticks}");
} }
[Test] [Test]
@@ -59,6 +61,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
new MediaCover.MediaCover { CoverType = MediaCoverTypes.Banner } new MediaCover.MediaCover { CoverType = MediaCoverTypes.Banner }
}; };
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "NonExistant.mp4");
var fileInfo = new FileInfo(path);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFileInfo(It.IsAny<string>()))
.Returns(fileInfo);
Subject.ConvertToLocalUrls(12, covers); Subject.ConvertToLocalUrls(12, covers);
covers.Single().Url.Should().Be("/MediaCover/12/banner.jpg"); covers.Single().Url.Should().Be("/MediaCover/12/banner.jpg");
@@ -364,7 +364,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{ {
_namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.FULL}"; _namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.FULL}";
_movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() _movieFile.MediaInfo = new MediaInfoModel()
{ {
VideoFormat = "AVC", VideoFormat = "AVC",
AudioFormat = "DTS", AudioFormat = "DTS",
@@ -376,12 +376,33 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be("South.Park.H264.DTS[EN+ES].[EN+ES+IT]"); .Should().Be("South.Park.H264.DTS[EN+ES].[EN+ES+IT]");
} }
[TestCase("Norwegian Bokmal", "NB")]
[TestCase("Swedis", "SV")]
[TestCase("Chinese", "ZH")]
public void should_format_languagecodes_properly(string language, string code)
{
_namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.FULL}";
_movieFile.MediaInfo = new MediaInfoModel()
{
VideoCodec = "AVC",
AudioFormat = "DTS",
AudioChannels = 6,
AudioLanguages = "English",
Subtitles = language,
SchemaRevision = 3
};
Subject.BuildFileName(_movie, _movieFile)
.Should().Be($"South.Park.X264.DTS.[{code}]");
}
[Test] [Test]
public void should_exclude_english_in_mediainfo_audio_language() public void should_exclude_english_in_mediainfo_audio_language()
{ {
_namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.FULL}"; _namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.FULL}";
_movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() _movieFile.MediaInfo = new MediaInfoModel()
{ {
VideoFormat = "AVC", VideoFormat = "AVC",
AudioFormat = "DTS", AudioFormat = "DTS",
@@ -398,7 +419,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{ {
_namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.3D}.{MediaInfo.Simple}"; _namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.3D}.{MediaInfo.Simple}";
_movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() _movieFile.MediaInfo = new MediaInfoModel()
{ {
VideoFormat = "AVC", VideoFormat = "AVC",
VideoMultiViewCount = 2, VideoMultiViewCount = 2,
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.GetMovie(title); Subject.GetMovie(title);
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
.Verify(s => s.FindByTitle(Parser.Parser.ParseMovieTitle(title, false).MovieTitle), Times.Once()); .Verify(s => s.FindByTitle(Parser.Parser.ParseMovieTitle(title, false).MovieTitle, It.IsAny<int>(), null, null, null), Times.Once());
} }
/*[Test] /*[Test]
@@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.Map(_parsedMovieInfo, "", null); Subject.Map(_parsedMovieInfo, "", null);
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
.Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>()), Times.Once()); .Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>(), null, null, null), Times.Once());
} }
[Test] [Test]
@@ -21,6 +21,7 @@ namespace NzbDrone.Core.Annotations
public Type SelectOptions { get; set; } public Type SelectOptions { get; set; }
public string Section { get; set; } public string Section { get; set; }
public HiddenType Hidden { get; set; } public HiddenType Hidden { get; set; }
public PrivacyLevel Privacy { get; set; }
public string RequestAction { get; set; } public string RequestAction { get; set; }
} }
@@ -62,4 +63,12 @@ namespace NzbDrone.Core.Annotations
Hidden, Hidden,
HiddenIfNotSet HiddenIfNotSet
} }
public enum PrivacyLevel
{
Normal,
Password,
ApiKey,
UserName
}
} }
@@ -18,6 +18,7 @@ namespace NzbDrone.Core.Blacklisting
PagingSpec<Blacklist> Paged(PagingSpec<Blacklist> pagingSpec); PagingSpec<Blacklist> Paged(PagingSpec<Blacklist> pagingSpec);
List<Blacklist> GetByMovieId(int movieId); List<Blacklist> GetByMovieId(int movieId);
void Delete(int id); void Delete(int id);
void Delete(List<int> ids);
} }
public class BlacklistService : IBlacklistService, public class BlacklistService : IBlacklistService,
@@ -76,6 +77,11 @@ namespace NzbDrone.Core.Blacklisting
_blacklistRepository.Delete(id); _blacklistRepository.Delete(id);
} }
public void Delete(List<int> ids)
{
_blacklistRepository.DeleteMany(ids);
}
private bool SameNzb(Blacklist item, ReleaseInfo release) private bool SameNzb(Blacklist item, ReleaseInfo release)
{ {
if (item.PublishedDate == release.PublishDate) if (item.PublishedDate == release.PublishDate)
@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(185)]
public class add_alternative_title_indices : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.Index().OnTable("AlternativeTitles").OnColumn("CleanTitle");
Create.Index().OnTable("MovieTranslations").OnColumn("CleanTitle");
}
}
}
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the deluge json url, see http://[host]:[port]/[urlBase]/json")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the deluge json url, see http://[host]:[port]/[urlBase]/json")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)] [FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
@@ -36,10 +36,10 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; } public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(2, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)] [FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
@@ -1,4 +1,4 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@@ -42,10 +42,10 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Hadouken url, e.g. http://[host]:[port]/[urlBase]/api")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Hadouken url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)] [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)] [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)]
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBVortex url, e.g. http://[host]:[port]/[urlBase]/api")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBVortex url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox)] [FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; } public string ApiKey { get; set; }
[FieldDefinition(4, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] [FieldDefinition(4, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
@@ -45,10 +45,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)] [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
@@ -39,10 +39,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)] [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
@@ -54,13 +54,13 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox)] [FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; } public string ApiKey { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(5, Label = "Password", Type = FieldType.Password)] [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
@@ -44,10 +44,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)] [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")]
@@ -43,10 +43,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)] [FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; } public bool UseSsl { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(5, Label = "Password", Type = FieldType.Password)] [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional.")] [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional.")]
@@ -37,10 +37,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the uTorrent url, e.g. http://[host]:[port]/[urlBase]/api")] [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the uTorrent url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)] [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)] [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
+1 -1
View File
@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Extras
var sourcePath = localMovie.Path; var sourcePath = localMovie.Path;
var sourceFolder = _diskProvider.GetParentFolder(sourcePath); var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly).Where(f => f != localMovie.Path); var files = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories).Where(f => f != localMovie.Path);
var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.Trim(' ', '.')) .Select(e => e.Trim(' ', '.'))
@@ -10,11 +10,13 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Credits; using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.Movies.Translations;
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{ {
@@ -25,11 +27,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
private readonly IDetectXbmcNfo _detectNfo; private readonly IDetectXbmcNfo _detectNfo;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly ICreditService _creditService; private readonly ICreditService _creditService;
private readonly IMovieTranslationService _movieTranslationsService;
public XbmcMetadata(IDetectXbmcNfo detectNfo, public XbmcMetadata(IDetectXbmcNfo detectNfo,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IMapCoversToLocal mediaCoverService, IMapCoversToLocal mediaCoverService,
ICreditService creditService, ICreditService creditService,
IMovieTranslationService movieTranslationsService,
Logger logger) Logger logger)
{ {
_logger = logger; _logger = logger;
@@ -37,6 +41,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
_diskProvider = diskProvider; _diskProvider = diskProvider;
_detectNfo = detectNfo; _detectNfo = detectNfo;
_creditService = creditService; _creditService = creditService;
_movieTranslationsService = movieTranslationsService;
} }
private static readonly Regex MovieImagesRegex = new Regex(@"^(?<type>poster|banner|fanart|clearart|discart|keyart|landscape|logo|backdrop|clearlogo)\.(?:png|jpe?g)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex MovieImagesRegex = new Regex(@"^(?<type>poster|banner|fanart|clearart|discart|keyart|landscape|logo|backdrop|clearlogo)\.(?:png|jpe?g)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -112,6 +117,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
if (Settings.MovieMetadata) if (Settings.MovieMetadata)
{ {
_logger.Debug("Generating Movie Metadata for: {0}", Path.Combine(movie.Path, movieFile.RelativePath)); _logger.Debug("Generating Movie Metadata for: {0}", Path.Combine(movie.Path, movieFile.RelativePath));
var movieMetadataLanguage = (Settings.MovieMetadataLanguage == (int)Language.Original) ?
(int)movie.OriginalLanguage :
Settings.MovieMetadataLanguage;
var movieTranslations = _movieTranslationsService.GetAllTranslationsForMovie(movie.Id);
var selectedSettingsLanguage = Language.FindById(movieMetadataLanguage);
var movieTranslation = movieTranslations.FirstOrDefault(mt => mt.Language == selectedSettingsLanguage);
var watched = GetExistingWatchedStatus(movie, movieFile.RelativePath); var watched = GetExistingWatchedStatus(movie, movieFile.RelativePath);
var sb = new StringBuilder(); var sb = new StringBuilder();
@@ -128,14 +142,14 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
var details = new XElement("movie"); var details = new XElement("movie");
details.Add(new XElement("title", movie.Title)); details.Add(new XElement("title", movieTranslation?.Title ?? movie.Title));
if (movie.Ratings != null && movie.Ratings.Votes > 0) if (movie.Ratings != null && movie.Ratings.Votes > 0)
{ {
details.Add(new XElement("rating", movie.Ratings.Value)); details.Add(new XElement("rating", movie.Ratings.Value));
} }
details.Add(new XElement("plot", movie.Overview)); details.Add(new XElement("plot", movieTranslation?.Overview ?? movie.Overview));
details.Add(new XElement("id", movie.ImdbId)); details.Add(new XElement("id", movie.ImdbId));
details.Add(new XElement("tmdbid", movie.TmdbId)); details.Add(new XElement("tmdbid", movie.TmdbId));
@@ -1,5 +1,6 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Languages;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -20,6 +21,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{ {
MovieMetadata = true; MovieMetadata = true;
MovieMetadataURL = false; MovieMetadataURL = false;
MovieMetadataLanguage = (int)Language.English;
MovieImages = true; MovieImages = true;
UseMovieNfo = false; UseMovieNfo = false;
} }
@@ -30,10 +32,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
[FieldDefinition(1, Label = "Movie Metadata URL", Type = FieldType.Checkbox, HelpText = "Radarr will write the tmdb/imdb url in the .nfo file", Advanced = true)] [FieldDefinition(1, Label = "Movie Metadata URL", Type = FieldType.Checkbox, HelpText = "Radarr will write the tmdb/imdb url in the .nfo file", Advanced = true)]
public bool MovieMetadataURL { get; set; } public bool MovieMetadataURL { get; set; }
[FieldDefinition(2, Label = "Movie Images", Type = FieldType.Checkbox)] [FieldDefinition(2, Label = "Metadata Language", Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), HelpText = "Radarr will write metadata in the selected language if available")]
public int MovieMetadataLanguage { get; set; }
[FieldDefinition(3, Label = "Movie Images", Type = FieldType.Checkbox)]
public bool MovieImages { get; set; } public bool MovieImages { get; set; }
[FieldDefinition(3, Label = "Use Movie.nfo", Type = FieldType.Checkbox, HelpText = "Radarr will write metadata to movie.nfo instead of the default <movie-filename>.nfo")] [FieldDefinition(4, Label = "Use Movie.nfo", Type = FieldType.Checkbox, HelpText = "Radarr will write metadata to movie.nfo instead of the default <movie-filename>.nfo")]
public bool UseMovieNfo { get; set; } public bool UseMovieNfo { get; set; }
public bool IsValid => true; public bool IsValid => true;
@@ -0,0 +1,59 @@
using System;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerLongTermStatusCheck : HealthCheckBase
{
private readonly IIndexerFactory _providerFactory;
private readonly IIndexerStatusService _providerStatusService;
public IndexerLongTermStatusCheck(IIndexerFactory providerFactory,
IIndexerStatusService providerStatusService,
ILocalizationService localizationService)
: base(localizationService)
{
_providerFactory = providerFactory;
_providerStatusService = providerStatusService;
}
public override HealthCheck Check()
{
var enabledProviders = _providerFactory.GetAvailableProviders();
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
i => i.Definition.Id,
s => s.ProviderId,
(i, s) => new { Provider = i, Status = s })
.Where(p => p.Status.InitialFailure.HasValue &&
p.Status.InitialFailure.Value.Before(
DateTime.UtcNow.AddHours(-6)))
.ToList();
if (backOffProviders.Empty())
{
return new HealthCheck(GetType());
}
if (backOffProviders.Count == enabledProviders.Count)
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("IndexerLongTermStatusCheckAllClientMessage"),
"#indexers-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#indexers-are-unavailable-due-to-failures");
}
}
}
@@ -1,3 +1,4 @@
using System;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@@ -25,10 +26,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
{ {
var enabledProviders = _providerFactory.GetAvailableProviders(); var enabledProviders = _providerFactory.GetAvailableProviders();
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
i => i.Definition.Id, i => i.Definition.Id,
s => s.ProviderId, s => s.ProviderId,
(i, s) => new { Provider = i, Status = s }) (i, s) => new { Provider = i, Status = s })
.ToList(); .Where(p => p.Status.InitialFailure.HasValue &&
p.Status.InitialFailure.Value.After(
DateTime.UtcNow.AddHours(-6)))
.ToList();
if (backOffProviders.Empty()) if (backOffProviders.Empty())
{ {
@@ -37,10 +41,17 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (backOffProviders.Count == enabledProviders.Count) if (backOffProviders.Count == enabledProviders.Count)
{ {
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("IndexerStatusCheckAllClientMessage"), "#indexers-are-unavailable-due-to-failures"); return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("IndexerStatusCheckAllClientMessage"),
"#indexers-are-unavailable-due-to-failures");
} }
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#indexers-are-unavailable-due-to-failures"); return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#indexers-are-unavailable-due-to-failures");
} }
} }
} }
@@ -9,9 +9,9 @@ namespace NzbDrone.Core.ImportLists.CouchPotato
{ {
public override string Name => "CouchPotato"; public override string Name => "CouchPotato";
public override ImportListType ListType => ImportListType.Program; public override ImportListSource ListType => ImportListSource.Program;
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public CouchPotatoImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) public CouchPotatoImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger) : base(httpClient, importListStatusService, configService, parsingService, logger)
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.ImportLists.CouchPotato
[FieldDefinition(2, Label = "CouchPotato Url Base", HelpText = "If you have CouchPotato configured via reverse proxy put the base path here. e.g. couchpotato. Leave blank for no base URL.")] [FieldDefinition(2, Label = "CouchPotato Url Base", HelpText = "If you have CouchPotato configured via reverse proxy put the base path here. e.g. couchpotato. Leave blank for no base URL.")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(3, Label = "CouchPotato API Key", HelpText = "CouchPotato API Key. This can found within Settings > General")] [FieldDefinition(3, Label = "CouchPotato API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "CouchPotato API Key. This can found within Settings > General")]
public string ApiKey { get; set; } public string ApiKey { get; set; }
[FieldDefinition(4, Label = "Only Wanted", HelpText = "Only add wanted movies.", Type = FieldType.Checkbox)] [FieldDefinition(4, Label = "Only Wanted", HelpText = "Only add wanted movies.", Type = FieldType.Checkbox)]
+2 -2
View File
@@ -5,9 +5,9 @@ namespace NzbDrone.Core.ImportLists
public interface IImportList : IProvider public interface IImportList : IProvider
{ {
bool Enabled { get; } bool Enabled { get; }
bool EnableAuto { get; } ImportListType EnableAuto { get; }
ImportListType ListType { get; } ImportListSource ListType { get; }
ImportListFetchResult Fetch(); ImportListFetchResult Fetch();
} }
} }
@@ -31,9 +31,9 @@ namespace NzbDrone.Core.ImportLists
public abstract string Name { get; } public abstract string Name { get; }
public abstract ImportListType ListType { get; } public abstract ImportListSource ListType { get; }
public abstract bool Enabled { get; } public abstract bool Enabled { get; }
public abstract bool EnableAuto { get; } public abstract ImportListType EnableAuto { get; }
public abstract ImportListFetchResult Fetch(); public abstract ImportListFetchResult Fetch();
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.ImportLists
{ {
Name = GetType().Name, Name = GetType().Name,
Enabled = config.Validate().IsValid && Enabled, Enabled = config.Validate().IsValid && Enabled,
EnableAuto = true, EnableAuto = ImportListType.Automatic,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = config Settings = config
}; };
@@ -12,7 +12,7 @@ namespace NzbDrone.Core.ImportLists
} }
public bool Enabled { get; set; } public bool Enabled { get; set; }
public bool EnableAuto { get; set; } public ImportListType EnableAuto { get; set; }
public bool ShouldMonitor { get; set; } public bool ShouldMonitor { get; set; }
public MovieStatusType MinimumAvailability { get; set; } public MovieStatusType MinimumAvailability { get; set; }
public int ProfileId { get; set; } public int ProfileId { get; set; }
@@ -20,6 +20,6 @@ namespace NzbDrone.Core.ImportLists
public bool SearchOnAdd { get; set; } public bool SearchOnAdd { get; set; }
public override bool Enable => Enabled; public override bool Enable => Enabled;
public ImportListType ListType { get; set; } public ImportListSource ListType { get; set; }
} }
} }
@@ -0,0 +1,11 @@
namespace NzbDrone.Core.ImportLists
{
public enum ImportListSource
{
Program,
TMDB,
Trakt,
Other,
Advanced
}
}
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.ImportLists
{ {
var result = _listFetcherAndParser.Fetch(); var result = _listFetcherAndParser.Fetch();
if (_importListFactory.Enabled().Where(a => ((ImportListDefinition)a.Definition).EnableAuto).Empty()) if (_importListFactory.Enabled().Where(a => ((ImportListDefinition)a.Definition).EnableAuto == ImportListType.Automatic).Empty())
{ {
_logger.Info("No auto enabled lists, skipping sync and cleaning"); _logger.Info("No auto enabled lists, skipping sync and cleaning");
return; return;
@@ -67,7 +67,7 @@ namespace NzbDrone.Core.ImportLists
private void ProcessMovieReport(ImportListDefinition importList, ImportListMovie report, List<ImportExclusion> listExclusions, List<int> dbMovies, List<Movie> moviesToAdd) private void ProcessMovieReport(ImportListDefinition importList, ImportListMovie report, List<ImportExclusion> listExclusions, List<int> dbMovies, List<Movie> moviesToAdd)
{ {
if (report.TmdbId == 0 || !importList.EnableAuto) if (report.TmdbId == 0 || importList.EnableAuto != ImportListType.Automatic)
{ {
return; return;
} }
@@ -2,10 +2,8 @@ namespace NzbDrone.Core.ImportLists
{ {
public enum ImportListType public enum ImportListType
{ {
Program, Manual,
TMDB, Automatic,
Trakt, Exclusion
Other,
Advanced
} }
} }
@@ -11,9 +11,9 @@ namespace NzbDrone.Core.ImportLists.RSSImport
{ {
public override string Name => "RSS List"; public override string Name => "RSS List";
public override ImportListType ListType => ImportListType.Advanced; public override ImportListSource ListType => ImportListSource.Advanced;
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public RSSImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) public RSSImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger) : base(httpClient, importListStatusService, configService, parsingService, logger)
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.ImportLists.RSSImport
{ {
Name = "IMDb List", Name = "IMDb List",
Enabled = Enabled, Enabled = Enabled,
EnableAuto = true, EnableAuto = ImportListType.Automatic,
ProfileId = 1, ProfileId = 1,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = new RSSImportSettings { Link = "https://rss.imdb.com/list/YOURLISTID" }, Settings = new RSSImportSettings { Link = "https://rss.imdb.com/list/YOURLISTID" },
@@ -42,7 +42,7 @@ namespace NzbDrone.Core.ImportLists.RSSImport
{ {
Name = "IMDb Watchlist", Name = "IMDb Watchlist",
Enabled = Enabled, Enabled = Enabled,
EnableAuto = true, EnableAuto = ImportListType.Automatic,
ProfileId = 1, ProfileId = 1,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = new RSSImportSettings { Link = "https://rss.imdb.com/user/IMDBUSERID/watchlist" }, Settings = new RSSImportSettings { Link = "https://rss.imdb.com/user/IMDBUSERID/watchlist" },
@@ -16,9 +16,9 @@ namespace NzbDrone.Core.ImportLists.Radarr
private readonly IRadarrV3Proxy _radarrV3Proxy; private readonly IRadarrV3Proxy _radarrV3Proxy;
public override string Name => "Radarr"; public override string Name => "Radarr";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override ImportListType ListType => ImportListType.Program; public override ImportListSource ListType => ImportListSource.Program;
public RadarrImport(IRadarrV3Proxy radarrV3Proxy, public RadarrImport(IRadarrV3Proxy radarrV3Proxy,
IImportListStatusService importListStatusService, IImportListStatusService importListStatusService,
@@ -31,7 +31,7 @@ namespace NzbDrone.Core.ImportLists.Radarr
[FieldDefinition(0, Label = "Full URL", HelpText = "URL, including port, of the Radarr V3 instance to import from")] [FieldDefinition(0, Label = "Full URL", HelpText = "URL, including port, of the Radarr V3 instance to import from")]
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "API Key", HelpText = "Apikey of the Radarr V3 instance to import from")] [FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "Apikey of the Radarr V3 instance to import from")]
public string ApiKey { get; set; } public string ApiKey { get; set; }
[FieldDefinition(2, Type = FieldType.Device, RequestAction = "getProfiles", Label = "Profiles", HelpText = "Profiles from the source instance to import from")] [FieldDefinition(2, Type = FieldType.Device, RequestAction = "getProfiles", Label = "Profiles", HelpText = "Profiles from the source instance to import from")]
@@ -11,9 +11,9 @@ namespace NzbDrone.Core.ImportLists.RadarrList
{ {
public override string Name => "Custom Lists"; public override string Name => "Custom Lists";
public override ImportListType ListType => ImportListType.Advanced; public override ImportListSource ListType => ImportListSource.Advanced;
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public RadarrListImport(IHttpClient httpClient, public RadarrListImport(IHttpClient httpClient,
IImportListStatusService importListStatusService, IImportListStatusService importListStatusService,
@@ -14,9 +14,9 @@ namespace NzbDrone.Core.ImportLists.RadarrList2.IMDbList
public override string Name => "IMDb Lists"; public override string Name => "IMDb Lists";
public override ImportListType ListType => ImportListType.Other; public override ImportListSource ListType => ImportListSource.Other;
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public IMDbListImport(IRadarrCloudRequestBuilder requestBuilder, public IMDbListImport(IRadarrCloudRequestBuilder requestBuilder,
IHttpClient httpClient, IHttpClient httpClient,
@@ -42,7 +42,7 @@ namespace NzbDrone.Core.ImportLists.RadarrList2.IMDbList
{ {
Name = "IMDb Top 250", Name = "IMDb Top 250",
Enabled = Enabled, Enabled = Enabled,
EnableAuto = true, EnableAuto = ImportListType.Automatic,
ProfileId = 1, ProfileId = 1,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = new IMDbListSettings { ListId = "top250" }, Settings = new IMDbListSettings { ListId = "top250" },
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.ImportLists.RadarrList2.IMDbList
{ {
Name = "IMDb Popular Movies", Name = "IMDb Popular Movies",
Enabled = Enabled, Enabled = Enabled,
EnableAuto = true, EnableAuto = ImportListType.Automatic,
ProfileId = 1, ProfileId = 1,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = new IMDbListSettings { ListId = "popular" }, Settings = new IMDbListSettings { ListId = "popular" },
@@ -32,9 +32,9 @@ namespace NzbDrone.Core.ImportLists.RadarrList2.IMDbList
if (_settings.ListId.StartsWith("ls", StringComparison.OrdinalIgnoreCase)) if (_settings.ListId.StartsWith("ls", StringComparison.OrdinalIgnoreCase))
{ {
//Parse TSV response from IMDB export //Parse TSV response from IMDB export
var row = importResponse.Content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); var rows = importResponse.Content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
movies = row.Skip(1).SelectList(m => new ImportListMovie { ImdbId = m.Split(',')[1] }); movies = rows.Skip(1).SelectList(m => m.Split(',')).Where(m => m.Length > 1).SelectList(i => new ImportListMovie { ImdbId = i[1] });
return movies; return movies;
} }
@@ -12,9 +12,9 @@ namespace NzbDrone.Core.ImportLists.RadarrList2.StevenLu
public override string Name => "StevenLu List"; public override string Name => "StevenLu List";
public override ImportListType ListType => ImportListType.Other; public override ImportListSource ListType => ImportListSource.Other;
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public StevenLu2Import(IRadarrCloudRequestBuilder requestBuilder, public StevenLu2Import(IRadarrCloudRequestBuilder requestBuilder,
IHttpClient httpClient, IHttpClient httpClient,
@@ -9,9 +9,9 @@ namespace NzbDrone.Core.ImportLists.StevenLu
{ {
public override string Name => "StevenLu Custom"; public override string Name => "StevenLu Custom";
public override ImportListType ListType => ImportListType.Advanced; public override ImportListSource ListType => ImportListSource.Advanced;
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public StevenLuImport(IHttpClient httpClient, public StevenLuImport(IHttpClient httpClient,
IImportListStatusService importListStatusService, IImportListStatusService importListStatusService,
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.Collection
public override string Name => "TMDb Collection"; public override string Name => "TMDb Collection";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IParseImportListResponse GetParser() public override IParseImportListResponse GetParser()
{ {
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.List
public override string Name => "TMDb List"; public override string Name => "TMDb List";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IParseImportListResponse GetParser() public override IParseImportListResponse GetParser()
{ {
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.Person
public override string Name => "TMDb Person"; public override string Name => "TMDb Person";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IParseImportListResponse GetParser() public override IParseImportListResponse GetParser()
{ {
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.Popular
public override string Name => "TMDb Popular"; public override string Name => "TMDb Popular";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IParseImportListResponse GetParser() public override IParseImportListResponse GetParser()
{ {
@@ -10,7 +10,7 @@ namespace NzbDrone.Core.ImportLists.TMDb
public abstract class TMDbImportListBase<TSettings> : HttpImportListBase<TSettings> public abstract class TMDbImportListBase<TSettings> : HttpImportListBase<TSettings>
where TSettings : TMDbSettingsBase<TSettings>, new() where TSettings : TMDbSettingsBase<TSettings>, new()
{ {
public override ImportListType ListType => ImportListType.TMDB; public override ImportListSource ListType => ImportListSource.TMDB;
public readonly ISearchForNewMovie _skyhookProxy; public readonly ISearchForNewMovie _skyhookProxy;
public readonly IHttpRequestBuilderFactory _requestBuilder; public readonly IHttpRequestBuilderFactory _requestBuilder;
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.User
public override string Name => "TMDb User"; public override string Name => "TMDb User";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IParseImportListResponse GetParser() public override IParseImportListResponse GetParser()
{ {
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.List
public override string Name => "Trakt List"; public override string Name => "Trakt List";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IImportListRequestGenerator GetRequestGenerator() public override IImportListRequestGenerator GetRequestGenerator()
{ {
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.List
{ {
} }
[FieldDefinition(1, Label = "Username", HelpText = "Username for the List to import from")] [FieldDefinition(1, Label = "Username", Privacy = PrivacyLevel.UserName, HelpText = "Username for the List to import from")]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(2, Label = "List Name", HelpText = "List name for import, list must be public or you must have access to the list")] [FieldDefinition(2, Label = "List Name", HelpText = "List name for import, list must be public or you must have access to the list")]
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular
public override string Name => "Trakt Popular List"; public override string Name => "Trakt Popular List";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IParseImportListResponse GetParser() public override IParseImportListResponse GetParser()
{ {
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.ImportLists.Trakt
{ {
public ITraktProxy _traktProxy; public ITraktProxy _traktProxy;
private readonly IImportListRepository _importListRepository; private readonly IImportListRepository _importListRepository;
public override ImportListType ListType => ImportListType.Trakt; public override ImportListSource ListType => ImportListSource.Trakt;
protected TraktImportBase(IImportListRepository importListRepository, protected TraktImportBase(IImportListRepository importListRepository,
ITraktProxy traktProxy, ITraktProxy traktProxy,
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.User
public override string Name => "Trakt User"; public override string Name => "Trakt User";
public override bool Enabled => true; public override bool Enabled => true;
public override bool EnableAuto => false; public override ImportListType EnableAuto => ImportListType.Manual;
public override IImportListRequestGenerator GetRequestGenerator() public override IImportListRequestGenerator GetRequestGenerator()
{ {
@@ -33,10 +33,10 @@ namespace NzbDrone.Core.Indexers.AwesomeHD
[FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since you Passkey will be sent to that host.")] [FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since you Passkey will be sent to that host.")]
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
[FieldDefinition(1, Type = FieldType.TagSelect, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)] [FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; } public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(2, Label = "Passkey")] [FieldDefinition(2, Label = "Passkey", Privacy = PrivacyLevel.ApiKey)]
public string Passkey { get; set; } public string Passkey { get; set; }
[FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
@@ -39,13 +39,13 @@ namespace NzbDrone.Core.Indexers.FileList
RequiredFlags = new List<int>(); RequiredFlags = new List<int>();
} }
[FieldDefinition(0, Label = "Username")] [FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(1, Label = "Passkey")] [FieldDefinition(1, Label = "Passkey", Privacy = PrivacyLevel.ApiKey)]
public string Passkey { get; set; } public string Passkey { get; set; }
[FieldDefinition(2, Type = FieldType.TagSelect, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)] [FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; } public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] [FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
@@ -34,13 +34,13 @@ namespace NzbDrone.Core.Indexers.HDBits
RequiredFlags = new List<int>(); RequiredFlags = new List<int>();
} }
[FieldDefinition(0, Label = "Username")] [FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(1, Type = FieldType.TagSelect, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)] [FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; } public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(2, Label = "API Key")] [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; } public string ApiKey { get; set; }
[FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] [FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents
[FieldDefinition(0, Label = "Feed URL", HelpText = "The full RSS feed url generated by IPTorrents, using only the categories you selected (HD, SD, x264, etc ...)")] [FieldDefinition(0, Label = "Feed URL", HelpText = "The full RSS feed url generated by IPTorrents, using only the categories you selected (HD, SD, x264, etc ...)")]
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
[FieldDefinition(1, Type = FieldType.TagSelect, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)] [FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; } public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] [FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
@@ -69,10 +69,10 @@ namespace NzbDrone.Core.Indexers.Newznab
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)] [FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
public string ApiPath { get; set; } public string ApiPath { get; set; }
[FieldDefinition(1, Type = FieldType.TagSelect, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)] [FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; } public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(2, Label = "API Key")] [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; } public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable all categories", Advanced = true)] [FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable all categories", Advanced = true)]
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Indexers.Nyaa
[FieldDefinition(0, Label = "Website URL")] [FieldDefinition(0, Label = "Website URL")]
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
[FieldDefinition(1, Type = FieldType.TagSelect, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)] [FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; } public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(2, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")] [FieldDefinition(2, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")]

Some files were not shown because too many files have changed in this diff Show More