mirror of https://github.com/Radarr/Radarr
Merge branch 'develop' into NET8
This commit is contained in:
commit
0a4c526715
|
@ -0,0 +1,13 @@
|
|||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Radarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [7878],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
|
@ -126,6 +126,7 @@ coverage*.xml
|
|||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Radarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Radarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Radarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Radarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Radarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,7 +9,7 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.4.3'
|
||||
majorVersion: '5.5.2'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
|
|
@ -20,7 +20,7 @@ const monitoredOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
|
@ -42,7 +42,7 @@ const searchOnAddOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
},
|
||||
{
|
||||
key: 'yes',
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface CommandBody {
|
|||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
movieId?: number;
|
||||
movieIds?: number[];
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
|
|
@ -36,7 +36,7 @@ function AvailabilitySelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ function AvailabilitySelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
|
|
|
@ -17,6 +17,7 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
|||
import KeyValueListInput from './KeyValueListInput';
|
||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||
import MovieTagInput from './MovieTagInput';
|
||||
import NumberInput from './NumberInput';
|
||||
import OAuthInputConnector from './OAuthInputConnector';
|
||||
import PasswordInput from './PasswordInput';
|
||||
|
@ -89,6 +90,10 @@ function getComponent(type) {
|
|||
|
||||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.MOVIE_TAG:
|
||||
return MovieTagInput;
|
||||
|
||||
case inputTypes.TAG:
|
||||
return TagInputConnector;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectInput from './SelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function MovieMonitoredSelectInput(props) {
|
||||
const values = [...monitorOptions];
|
||||
|
@ -16,7 +16,7 @@ function MovieMonitoredSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -24,12 +24,12 @@ function MovieMonitoredSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
|
||||
interface MovieTagInputProps {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
onChange: ({
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function MovieTagInput(props: MovieTagInputProps) {
|
||||
const { value, onChange, ...otherProps } = props;
|
||||
const isArray = Array.isArray(value);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||
if (isArray) {
|
||||
onChange({ name, value: newValue });
|
||||
} else {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isArray, onChange]
|
||||
);
|
||||
|
||||
let finalValue: number[] = [];
|
||||
|
||||
if (isArray) {
|
||||
finalValue = value;
|
||||
} else if (value === 0) {
|
||||
finalValue = [];
|
||||
} else {
|
||||
finalValue = [value];
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||
<TagInputConnector
|
||||
{...otherProps}
|
||||
value={finalValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -27,6 +27,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'movieTag':
|
||||
return inputTypes.MOVIE_TAG;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
case 'tagSelect':
|
||||
|
|
|
@ -26,7 +26,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ const selectAppProps = createSelector(
|
|||
);
|
||||
|
||||
const selectIsPopulated = createSelector(
|
||||
(state) => state.movies.isPopulated,
|
||||
(state) => state.customFilters.isPopulated,
|
||||
(state) => state.tags.isPopulated,
|
||||
(state) => state.settings.ui.isPopulated,
|
||||
|
@ -56,6 +57,7 @@ const selectIsPopulated = createSelector(
|
|||
(state) => state.movieCollections.isPopulated,
|
||||
(state) => state.app.translations.isPopulated,
|
||||
(
|
||||
moviesIsPopulated,
|
||||
customFiltersIsPopulated,
|
||||
tagsIsPopulated,
|
||||
uiSettingsIsPopulated,
|
||||
|
@ -68,6 +70,7 @@ const selectIsPopulated = createSelector(
|
|||
translationsIsPopulated
|
||||
) => {
|
||||
return (
|
||||
moviesIsPopulated &&
|
||||
customFiltersIsPopulated &&
|
||||
tagsIsPopulated &&
|
||||
uiSettingsIsPopulated &&
|
||||
|
@ -83,6 +86,7 @@ const selectIsPopulated = createSelector(
|
|||
);
|
||||
|
||||
const selectErrors = createSelector(
|
||||
(state) => state.movies.error,
|
||||
(state) => state.customFilters.error,
|
||||
(state) => state.tags.error,
|
||||
(state) => state.settings.ui.error,
|
||||
|
@ -94,6 +98,7 @@ const selectErrors = createSelector(
|
|||
(state) => state.movieCollections.error,
|
||||
(state) => state.app.translations.error,
|
||||
(
|
||||
moviesError,
|
||||
customFiltersError,
|
||||
tagsError,
|
||||
uiSettingsError,
|
||||
|
@ -106,6 +111,7 @@ const selectErrors = createSelector(
|
|||
translationsError
|
||||
) => {
|
||||
const hasError = !!(
|
||||
moviesError ||
|
||||
customFiltersError ||
|
||||
tagsError ||
|
||||
uiSettingsError ||
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "minimal-ui"
|
||||
"display": "standalone"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
export default function useModalOpenState(
|
||||
initialState: boolean
|
||||
): [boolean, () => void, () => void] {
|
||||
const [isOpen, setOpen] = useState(initialState);
|
||||
|
||||
const setModalOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
|
||||
const setModalClosed = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
return [isOpen, setModalOpen, setModalClosed];
|
||||
}
|
|
@ -17,6 +17,7 @@ export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
|||
export const LANGUAGE_SELECT = 'languageSelect';
|
||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||
export const SELECT = 'select';
|
||||
export const MOVIE_TAG = 'movieTag';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
export const TAG = 'tag';
|
||||
export const TEXT = 'text';
|
||||
|
@ -45,6 +46,7 @@ export const all = [
|
|||
INDEXER_FLAGS_SELECT,
|
||||
LANGUAGE_SELECT,
|
||||
SELECT,
|
||||
MOVIE_TAG,
|
||||
DYNAMIC_SELECT,
|
||||
TAG,
|
||||
TEXT,
|
||||
|
|
|
@ -104,7 +104,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
|
||||
useEffect(
|
||||
() => {
|
||||
if (allowMovieChange && movie && quality && languages) {
|
||||
if (allowMovieChange && movie && quality && languages && size > 0) {
|
||||
onSelectedChange({
|
||||
id,
|
||||
hasMovieFileId: !!movieFileId,
|
||||
|
|
|
@ -44,6 +44,7 @@ import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
|||
import MovieIndexFooter from './MovieIndexFooter';
|
||||
import MovieIndexRefreshMovieButton from './MovieIndexRefreshMovieButton';
|
||||
import MovieIndexSearchButton from './MovieIndexSearchButton';
|
||||
import MovieIndexSearchMenuItem from './MovieIndexSearchMenuItem';
|
||||
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
||||
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
||||
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
||||
|
@ -247,6 +248,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||
<MovieIndexSearchButton
|
||||
isSelectMode={isSelectMode}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
overflowComponent={MovieIndexSearchMenuItem}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
|
|
|
@ -16,6 +16,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||
interface MovieIndexSearchButtonProps {
|
||||
isSelectMode: boolean;
|
||||
selectedFilterKey: string;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
}
|
||||
|
||||
function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
||||
import { MOVIE_SEARCH } from 'Commands/commandNames';
|
||||
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
|
||||
interface MovieIndexSearchMenuItemProps {
|
||||
isSelectMode: boolean;
|
||||
selectedFilterKey: string;
|
||||
}
|
||||
|
||||
function MovieIndexSearchMenuItem(props: MovieIndexSearchMenuItemProps) {
|
||||
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
|
||||
const {
|
||||
items,
|
||||
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
|
||||
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isSelectMode, selectedFilterKey } = props;
|
||||
const [selectState] = useSelect();
|
||||
const { selectedState } = selectState;
|
||||
|
||||
const selectedMovieIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const moviesToSearch =
|
||||
isSelectMode && selectedMovieIds.length > 0
|
||||
? selectedMovieIds
|
||||
: items.map((m) => m.id);
|
||||
|
||||
const searchIndexLabel =
|
||||
selectedFilterKey === 'all'
|
||||
? translate('SearchAll')
|
||||
: translate('SearchFiltered');
|
||||
|
||||
const searchSelectLabel =
|
||||
selectedMovieIds.length > 0
|
||||
? translate('SearchSelected')
|
||||
: translate('SearchAll');
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: MOVIE_SEARCH,
|
||||
movieIds: moviesToSearch,
|
||||
})
|
||||
);
|
||||
}, [dispatch, moviesToSearch]);
|
||||
|
||||
return (
|
||||
<PageToolbarOverflowMenuItem
|
||||
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||
isSpinning={isSearching}
|
||||
isDisabled={!items.length}
|
||||
iconName={icons.SEARCH}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieIndexSearchMenuItem;
|
|
@ -34,7 +34,7 @@ const monitoredOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
|
|
|
@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
|
|||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('CustomFormatsSettingsTriggerInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
specifications.map((tag) => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
|
@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteDownloadClientPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
@ -199,6 +201,12 @@ class EditDownloadClientModalContent extends Component {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -239,6 +247,7 @@ EditDownloadClientModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
|
||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||
setDownloadClientValue,
|
||||
setDownloadClientFieldValue,
|
||||
saveDownloadClient,
|
||||
testDownloadClient
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
|
@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
|||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
testDownloadClient: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ const enableOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
text-align: center;
|
||||
font-weight: lighter;
|
||||
font-size: 24px;
|
||||
|
|
|
@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditImportListModalContent.css';
|
||||
|
@ -33,6 +34,7 @@ function EditImportListModalContent(props) {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteImportListPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
@ -234,6 +236,12 @@ function EditImportListModalContent(props) {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -274,6 +282,7 @@ EditImportListModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteImportListPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveImportList,
|
||||
setImportListFieldValue,
|
||||
setImportListValue,
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditImportListModalContent from './EditImportListModalContent';
|
||||
|
||||
|
@ -33,7 +39,8 @@ const mapDispatchToProps = {
|
|||
setImportListValue,
|
||||
setImportListFieldValue,
|
||||
saveImportList,
|
||||
testImportList
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditImportListModalContentConnector extends Component {
|
||||
|
@ -66,6 +73,10 @@ class EditImportListModalContentConnector extends Component {
|
|||
this.props.testImportList({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -75,6 +86,7 @@ class EditImportListModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -92,6 +104,7 @@ EditImportListModalContentConnector.propTypes = {
|
|||
setImportListFieldValue: PropTypes.func.isRequired,
|
||||
saveImportList: PropTypes.func.isRequired,
|
||||
testImportList: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ const enableOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
|
|
@ -32,7 +32,7 @@ const enableOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
|
|
@ -72,15 +72,15 @@ const fileNameTokens = [
|
|||
];
|
||||
|
||||
const movieTokens = [
|
||||
{ token: '{Movie Title}', example: 'Movie\'s Title' },
|
||||
{ token: '{Movie Title:DE}', example: 'Titel des Films' },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movies Title' },
|
||||
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The' },
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' },
|
||||
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας' },
|
||||
{ token: '{Movie Title}', example: 'Movie\'s Title', footNote: 1 },
|
||||
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: 1 },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: 1 },
|
||||
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The', footNote: 1 },
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
||||
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
||||
{ token: '{Movie Collection}', example: 'The Movie Collection' },
|
||||
{ token: '{Movie Collection}', example: 'The Movie Collection', footNote: 1 },
|
||||
{ token: '{Movie Certification}', example: 'R' },
|
||||
{ token: '{Release Year}', example: '2009' }
|
||||
];
|
||||
|
@ -112,15 +112,16 @@ const mediaInfoTokens = [
|
|||
];
|
||||
|
||||
const releaseGroupTokens = [
|
||||
{ token: '{Release Group}', example: 'Rls Grp' }
|
||||
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 }
|
||||
];
|
||||
|
||||
const editionTokens = [
|
||||
{ token: '{Edition Tags}', example: 'IMAX' }
|
||||
{ token: '{Edition Tags}', example: 'IMAX', footNote: 1 }
|
||||
];
|
||||
|
||||
const customFormatTokens = [
|
||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' }
|
||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' },
|
||||
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
|
||||
];
|
||||
|
||||
const originalTokens = [
|
||||
|
@ -267,7 +268,7 @@ class NamingModal extends Component {
|
|||
<FieldSet legend={translate('Movie')}>
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
movieTokens.map(({ token, example }) => {
|
||||
movieTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
|
@ -275,6 +276,7 @@ class NamingModal extends Component {
|
|||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
|
@ -284,6 +286,11 @@ class NamingModal extends Component {
|
|||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<InlineMarkdown data={translate('MovieFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('MovieID')}>
|
||||
|
@ -364,7 +371,7 @@ class NamingModal extends Component {
|
|||
<FieldSet legend={translate('ReleaseGroup')}>
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
releaseGroupTokens.map(({ token, example }) => {
|
||||
releaseGroupTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
|
@ -372,6 +379,7 @@ class NamingModal extends Component {
|
|||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
|
@ -381,12 +389,17 @@ class NamingModal extends Component {
|
|||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Edition')}>
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
editionTokens.map(({ token, example }) => {
|
||||
editionTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
|
@ -394,6 +407,7 @@ class NamingModal extends Component {
|
|||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
|
@ -403,6 +417,11 @@ class NamingModal extends Component {
|
|||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<InlineMarkdown data={translate('EditionFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('CustomFormats')}>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
.small {
|
||||
width: 480px;
|
||||
width: 490px;
|
||||
}
|
||||
|
||||
.large {
|
||||
|
@ -26,8 +26,8 @@
|
|||
|
||||
.token {
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
background-color: var(--popoverTitleBorderColor);
|
||||
padding: 6px;
|
||||
background-color: var(--popoverTitleBackgroundColor);
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,8 @@
|
|||
align-self: stretch;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
background-color: var(--popoverTitleBackgroundColor);
|
||||
padding: 6px;
|
||||
background-color: var(--popoverBodyBackgroundColor);
|
||||
|
||||
.footNote {
|
||||
padding: 2px;
|
||||
|
|
|
@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NotificationEventItems from './NotificationEventItems';
|
||||
import styles from './EditNotificationModalContent.css';
|
||||
|
@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteNotificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteNotificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveNotification,
|
||||
setNotificationFieldValue,
|
||||
setNotificationValue,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||
|
||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||
setNotificationValue,
|
||||
setNotificationFieldValue,
|
||||
saveNotification,
|
||||
testNotification
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditNotificationModalContentConnector extends Component {
|
||||
|
@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
|||
this.props.testNotification({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
|||
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||
saveNotification: PropTypes.func.isRequired,
|
||||
testNotification: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ export default function TagInUse(props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (count > 1 && labelPlural ) {
|
||||
if (count > 1 && labelPlural) {
|
||||
return (
|
||||
<div>
|
||||
{count} {labelPlural.toLowerCase()}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastTestData = null;
|
||||
|
||||
export function createCancelTestProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
|
@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
|||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isTesting: true }));
|
||||
|
||||
const testData = getProviderState(payload, getState, section);
|
||||
const {
|
||||
queryParams = {},
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const testData = getProviderState({ ...otherPayload }, getState, section);
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-testing the same provider without changes
|
||||
// force it to be tested.
|
||||
|
||||
if (_.isEqual(testData, lastTestData)) {
|
||||
params.forceTest = true;
|
||||
}
|
||||
|
||||
lastTestData = testData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/test`,
|
||||
url: `${url}/test?${$.param(params, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
|
@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
|||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastTestData = null;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isTesting: false,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Movie from 'Movie/Movie';
|
||||
|
||||
function createMultiMoviesSelector(movieIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movies.itemMap,
|
||||
(state: AppState) => state.movies.items,
|
||||
(itemMap, allMovies) => {
|
||||
return movieIds.reduce((acc: Movie[], movieId) => {
|
||||
const movie = allMovies[itemMap[movieId]];
|
||||
|
||||
if (movie) {
|
||||
acc.push(movie);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMultiMoviesSelector;
|
|
@ -10,15 +10,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.queued,
|
||||
.started,
|
||||
.ended {
|
||||
|
|
|
@ -2,14 +2,12 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'commandName': string;
|
||||
'duration': string;
|
||||
'ended': string;
|
||||
'queued': string;
|
||||
'started': string;
|
||||
'trigger': string;
|
||||
'triggerContent': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
|
@ -1,279 +0,0 @@
|
|||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status, message) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(props) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
} = props;
|
||||
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
|
||||
};
|
||||
}
|
||||
|
||||
class QueuedTaskRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
...getFormattedDates(props),
|
||||
isCancelConfirmModalOpen: false
|
||||
};
|
||||
|
||||
this._updateTimeoutId = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setUpdateTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
queued !== prevProps.queued ||
|
||||
started !== prevProps.started ||
|
||||
ended !== prevProps.ended
|
||||
) {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._updateTimeoutId) {
|
||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setUpdateTimer() {
|
||||
this._updateTimeoutId = setTimeout(() => {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
this.setUpdateTimer();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCancelPress = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onAbortCancel = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
clientUserAgent,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onCancelPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
queuedAt,
|
||||
startedAt,
|
||||
endedAt,
|
||||
isCancelConfirmModalOpen
|
||||
} = this.state;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon
|
||||
name={triggerIcon}
|
||||
title={titleCase(trigger)}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
{...getStatusIconProps(status, message)}
|
||||
/>
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
</span>
|
||||
{
|
||||
clientUserAgent ?
|
||||
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
|
||||
{translate('From')}: {clientUserAgent}
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||
>
|
||||
{queuedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.started}
|
||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||
>
|
||||
{startedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.ended}
|
||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||
>
|
||||
{endedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.duration}>
|
||||
{formatTimeSpan(duration)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
status === 'queued' &&
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={this.onCancelPress}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isCancelConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('Cancel')}
|
||||
message={translate('CancelPendingTask')}
|
||||
confirmLabel={translate('YesCancel')}
|
||||
cancelLabel={translate('NoLeaveIt')}
|
||||
onConfirm={onCancelPress}
|
||||
onCancel={this.onAbortCancel}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueuedTaskRow.propTypes = {
|
||||
trigger: PropTypes.string.isRequired,
|
||||
commandName: PropTypes.string.isRequired,
|
||||
queued: PropTypes.string.isRequired,
|
||||
started: PropTypes.string,
|
||||
ended: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
duration: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
clientUserAgent: PropTypes.string,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QueuedTaskRow;
|
|
@ -0,0 +1,238 @@
|
|||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CommandBody } from 'Commands/Command';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status: string, message: string | undefined) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`,
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(
|
||||
queued: string,
|
||||
started: string | undefined,
|
||||
ended: string | undefined,
|
||||
showRelativeDates: boolean,
|
||||
shortDateFormat: string
|
||||
) {
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
|
||||
};
|
||||
}
|
||||
|
||||
interface QueuedTimes {
|
||||
queuedAt: string;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
}
|
||||
|
||||
export interface QueuedTaskRowProps {
|
||||
id: number;
|
||||
trigger: string;
|
||||
commandName: string;
|
||||
queued: string;
|
||||
started?: string;
|
||||
ended?: string;
|
||||
status: string;
|
||||
duration?: string;
|
||||
message?: string;
|
||||
body: CommandBody;
|
||||
clientUserAgent?: string;
|
||||
}
|
||||
|
||||
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
||||
const {
|
||||
id,
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
body,
|
||||
clientUserAgent,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const [times, setTimes] = useState<QueuedTimes>(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
|
||||
const [
|
||||
isCancelConfirmModalOpen,
|
||||
openCancelConfirmModal,
|
||||
closeCancelConfirmModal,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
dispatch(cancelCommand({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
updateTimeTimeoutId.current = setTimeout(() => {
|
||||
setTimes(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
if (updateTimeTimeoutId.current) {
|
||||
clearTimeout(updateTimeTimeoutId.current);
|
||||
}
|
||||
};
|
||||
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
|
||||
|
||||
const { queuedAt, startedAt, endedAt } = times;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon name={triggerIcon} title={titleCase(trigger)} />
|
||||
|
||||
<Icon {...getStatusIconProps(status, message)} />
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<QueuedTaskRowNameCell
|
||||
commandName={commandName}
|
||||
body={body}
|
||||
clientUserAgent={clientUserAgent}
|
||||
/>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||
>
|
||||
{queuedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.started}
|
||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||
>
|
||||
{startedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.ended}
|
||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||
>
|
||||
{endedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.duration}>
|
||||
{formatTimeSpan(duration)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{status === 'queued' && (
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={openCancelConfirmModal}
|
||||
/>
|
||||
)}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isCancelConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('Cancel')}
|
||||
message={translate('CancelPendingTask')}
|
||||
confirmLabel={translate('YesCancel')}
|
||||
cancelLabel={translate('NoLeaveIt')}
|
||||
onConfirm={handleCancelPress}
|
||||
onCancel={closeCancelConfirmModal}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onCancelPress() {
|
||||
dispatch(cancelCommand({
|
||||
id: props.id
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
|
|
@ -0,0 +1,8 @@
|
|||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'commandName': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,65 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CommandBody } from 'Commands/Command';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import createMultiMoviesSelector from 'Store/Selectors/createMultiMoviesSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRowNameCell.css';
|
||||
|
||||
function formatTitles(titles: string[]) {
|
||||
if (!titles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (titles.length > 11) {
|
||||
return (
|
||||
<span title={titles.join(', ')}>
|
||||
{titles.slice(0, 10).join(', ')}, {titles.length - 10} more
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{titles.join(', ')}</span>;
|
||||
}
|
||||
|
||||
export interface QueuedTaskRowNameCellProps {
|
||||
commandName: string;
|
||||
body: CommandBody;
|
||||
clientUserAgent?: string;
|
||||
}
|
||||
|
||||
export default function QueuedTaskRowNameCell(
|
||||
props: QueuedTaskRowNameCellProps
|
||||
) {
|
||||
const { commandName, body, clientUserAgent } = props;
|
||||
const movieIds = [...(body.movieIds ?? [])];
|
||||
|
||||
if (body.movieId) {
|
||||
movieIds.push(body.movieId);
|
||||
}
|
||||
|
||||
const movies = useSelector(createMultiMoviesSelector(movieIds));
|
||||
const sortedMovies = movies.sort((a, b) =>
|
||||
a.sortTitle.localeCompare(b.sortTitle)
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
{sortedMovies.length ? (
|
||||
<span> - {formatTitles(sortedMovies.map((m) => m.title))}</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
{clientUserAgent ? (
|
||||
<span
|
||||
className={styles.userAgent}
|
||||
title={translate('TaskUserAgentTooltip')}
|
||||
>
|
||||
{translate('From')}: {clientUserAgent}
|
||||
</span>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: () => translate('Trigger'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function QueuedTasks(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueuedTaskRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
QueuedTasks.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default QueuedTasks;
|
|
@ -0,0 +1,74 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function QueuedTasks() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, items } = useSelector(
|
||||
(state: AppState) => state.commands
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCommands());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||
|
||||
{isPopulated && (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return <QueuedTaskRow key={item.id} {...item} />;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import QueuedTasks from './QueuedTasks';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.commands,
|
||||
(commands) => {
|
||||
return commands;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchCommands: fetchCommands
|
||||
};
|
||||
|
||||
class QueuedTasksConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchCommands();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QueuedTasks
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueuedTasksConnector.propTypes = {
|
||||
dispatchFetchCommands: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
||||
import QueuedTasks from './Queued/QueuedTasks';
|
||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||
|
||||
function Tasks() {
|
||||
|
@ -10,7 +10,7 @@ function Tasks() {
|
|||
<PageContent title={translate('Tasks')}>
|
||||
<PageContentBody>
|
||||
<ScheduledTasksConnector />
|
||||
<QueuedTasksConnector />
|
||||
<QueuedTasks />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
40
package.json
40
package.json
|
@ -11,7 +11,7 @@
|
|||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
||||
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
||||
"stylelint-windows": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc"
|
||||
},
|
||||
"repository": "https://github.com/Radarr/Radarr",
|
||||
"author": "Team Radarr",
|
||||
|
@ -31,9 +31,9 @@
|
|||
"@microsoft/signalr": "8.0.0",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.16.8",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@types/node": "18.19.31",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
|
@ -84,16 +84,16 @@
|
|||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"swiper": "8.3.2",
|
||||
"typescript": "4.9.5"
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.11",
|
||||
"@babel/eslint-parser": "7.22.11",
|
||||
"@babel/plugin-proposal-export-default-from": "7.22.5",
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.22.14",
|
||||
"@babel/preset-react": "7.22.5",
|
||||
"@babel/preset-typescript": "7.22.11",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-lazyload": "3.2.0",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
|
@ -101,31 +101,31 @@
|
|||
"@types/react-window": "1.8.5",
|
||||
"@types/redux-actions": "2.6.2",
|
||||
"@types/webpack-livereload-plugin": "2.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.32.1",
|
||||
"core-js": "3.37.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-json": "3.1.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"postcss": "8.4.23",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
|
|
|
@ -147,16 +147,46 @@
|
|||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<!--
|
||||
Set architecture to RuntimeInformation.ProcessArchitecture if not specified -->
|
||||
<Choose>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">
|
||||
<PropertyGroup>
|
||||
<Architecture>x64</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'">
|
||||
<PropertyGroup>
|
||||
<Architecture>x86</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">
|
||||
<PropertyGroup>
|
||||
<Architecture>arm64</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'">
|
||||
<PropertyGroup>
|
||||
<Architecture>arm</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup>
|
||||
<Architecture></Architecture>
|
||||
</PropertyGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
||||
'$(RuntimeIdentifier)' == ''">
|
||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
||||
'$(RuntimeIdentifier)' == ''">
|
||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
||||
|
|
|
@ -19,6 +19,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
|
||||
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||
[TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.IO;
|
|||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Test.Common;
|
||||
|
@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test
|
|||
[TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
|
||||
public void Clean_Path_Windows(string dirty, string clean)
|
||||
{
|
||||
|
@ -334,5 +335,30 @@ namespace NzbDrone.Common.Test
|
|||
result[2].Should().Be(@"TV");
|
||||
result[3].Should().Be(@"Series Title");
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test\")]
|
||||
[TestCase(@"C:\Test")]
|
||||
[TestCase(@"C:\Test\TV\")]
|
||||
[TestCase(@"C:\Test\TV")]
|
||||
public void IsPathValid_should_be_true(string path)
|
||||
{
|
||||
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test \")]
|
||||
[TestCase(@"C:\Test ")]
|
||||
[TestCase(@"C:\ Test\")]
|
||||
[TestCase(@"C:\ Test")]
|
||||
[TestCase(@"C:\Test \TV")]
|
||||
[TestCase(@"C:\ Test\TV")]
|
||||
[TestCase(@"C:\Test \TV\")]
|
||||
[TestCase(@"C:\ Test\TV\")]
|
||||
[TestCase(@" C:\Test\TV\")]
|
||||
[TestCase(@" C:\Test\TV")]
|
||||
|
||||
public void IsPathValid_should_be_false(string path)
|
||||
{
|
||||
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,12 @@ namespace NzbDrone.Common.Extensions
|
|||
|
||||
public static string CleanFilePath(this string path)
|
||||
{
|
||||
if (path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
// Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix.
|
||||
path = path.TrimEnd(' ');
|
||||
}
|
||||
|
||||
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
|
||||
|
||||
|
@ -38,10 +44,10 @@ namespace NzbDrone.Common.Extensions
|
|||
// UNC
|
||||
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
|
||||
{
|
||||
return info.FullName.TrimEnd('/', '\\', ' ');
|
||||
return info.FullName.TrimEnd('/', '\\');
|
||||
}
|
||||
|
||||
return info.FullName.TrimEnd('/').Trim('\\', ' ');
|
||||
return info.FullName.TrimEnd('/').Trim('\\');
|
||||
}
|
||||
|
||||
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
|
||||
|
@ -155,6 +161,23 @@ namespace NzbDrone.Common.Extensions
|
|||
return false;
|
||||
}
|
||||
|
||||
if (path.Trim() != path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
|
||||
while (directoryInfo != null)
|
||||
{
|
||||
if (directoryInfo.Name.Trim() != directoryInfo.Name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
directoryInfo = directoryInfo.Parent;
|
||||
}
|
||||
|
||||
if (validationType == PathValidationType.AnyOs)
|
||||
{
|
||||
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
|
||||
|
@ -292,6 +315,11 @@ namespace NzbDrone.Common.Extensions
|
|||
return processName;
|
||||
}
|
||||
|
||||
public static string CleanPath(this string path)
|
||||
{
|
||||
return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray());
|
||||
}
|
||||
|
||||
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return appFolderInfo.AppDataFolder;
|
||||
|
|
|
@ -9,6 +9,7 @@ using System.Net.Sockets;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
|
@ -30,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
||||
private readonly ICached<CredentialCache> _credentialCache;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
||||
ICreateManagedWebProxy createManagedWebProxy,
|
||||
ICertificateValidationService certificateValidationService,
|
||||
IUserAgentBuilder userAgentBuilder,
|
||||
ICacheManager cacheManager)
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_proxySettingsProvider = proxySettingsProvider;
|
||||
_createManagedWebProxy = createManagedWebProxy;
|
||||
|
@ -43,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
|
||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
||||
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||
|
@ -248,19 +254,27 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||
}
|
||||
|
||||
private static bool HasRoutableIPv4Address()
|
||||
private bool HasRoutableIPv4Address()
|
||||
{
|
||||
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
try
|
||||
{
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
||||
|
@ -284,7 +298,9 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
catch
|
||||
{
|
||||
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||
useIPv6 = !HasRoutableIPv4Address();
|
||||
var routableIPv4 = HasRoutableIPv4Address();
|
||||
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
|
||||
useIPv6 = !routableIPv4;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||
{
|
||||
[TestFixture]
|
||||
public class MultiLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||
{
|
||||
private CustomFormatInput _input;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_input = new CustomFormatInput
|
||||
{
|
||||
MovieInfo = Builder<ParsedMovieInfo>.CreateNew().Build(),
|
||||
Movie = Builder<Movie>.CreateNew().With(m => m.MovieMetadata.Value.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.English,
|
||||
Language.French
|
||||
},
|
||||
Filename = "Movie.Title.2024"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_one_language()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_different_language()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_when_one_language_matches()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_when_all_languages_do_not_match()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||
{
|
||||
[TestFixture]
|
||||
public class OriginalLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||
{
|
||||
private CustomFormatInput _input;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_input = new CustomFormatInput
|
||||
{
|
||||
MovieInfo = Builder<ParsedMovieInfo>.CreateNew().Build(),
|
||||
Movie = Builder<Movie>.CreateNew().With(m => m.MovieMetadata.Value.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.French
|
||||
},
|
||||
Filename = "Movie.Title.2024"
|
||||
};
|
||||
}
|
||||
|
||||
public void GivenLanguages(params Language[] languages)
|
||||
{
|
||||
_input.Languages = languages.ToList();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_same_single_language()
|
||||
{
|
||||
GivenLanguages(Language.English);
|
||||
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_different_single_language()
|
||||
{
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_same_single_language()
|
||||
{
|
||||
GivenLanguages(Language.English);
|
||||
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_negated_different_single_language()
|
||||
{
|
||||
Subject.Value = Language.Original.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||
{
|
||||
[TestFixture]
|
||||
public class SingleLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||
{
|
||||
private CustomFormatInput _input;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_input = new CustomFormatInput
|
||||
{
|
||||
MovieInfo = Builder<ParsedMovieInfo>.CreateNew().Build(),
|
||||
Movie = Builder<Movie>.CreateNew().With(m => m.MovieMetadata.Value.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.French
|
||||
},
|
||||
Filename = "Movie.Title.2024"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_same_language()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_different_language()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = false;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_match_negated_same_language()
|
||||
{
|
||||
Subject.Value = Language.French.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_match_negated_different_language()
|
||||
{
|
||||
Subject.Value = Language.Spanish.Id;
|
||||
Subject.Negate = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
Subject.Definition.Settings.As<QBittorrentSettings>().RecentMoviePriority = (int)QBittorrentPriority.First;
|
||||
}
|
||||
|
||||
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
|
||||
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, int maxInactiveSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
|
||||
{
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||
|
@ -118,7 +118,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
MaxRatio = maxRatio,
|
||||
MaxRatioEnabled = maxRatio >= 0,
|
||||
MaxSeedingTime = maxSeedingTime,
|
||||
MaxSeedingTimeEnabled = maxSeedingTime >= 0
|
||||
MaxSeedingTimeEnabled = maxSeedingTime >= 0,
|
||||
MaxInactiveSeedingTime = maxInactiveSeedingTime,
|
||||
MaxInactiveSeedingTimeEnabled = maxInactiveSeedingTime >= 0
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -609,7 +611,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
float ratio = 0.1f,
|
||||
float ratioLimit = -2,
|
||||
int seedingTime = 1,
|
||||
int seedingTimeLimit = -2)
|
||||
int seedingTimeLimit = -2,
|
||||
int inactiveSeedingTimeLimit = -2,
|
||||
long lastActivity = -1)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
|
@ -623,7 +627,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
SavePath = "",
|
||||
Ratio = ratio,
|
||||
RatioLimit = ratioLimit,
|
||||
SeedingTimeLimit = seedingTimeLimit
|
||||
SeedingTimeLimit = seedingTimeLimit,
|
||||
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit,
|
||||
LastActivity = lastActivity == -1 ? DateTimeOffset.UtcNow.ToUnixTimeSeconds() : lastActivity
|
||||
};
|
||||
|
||||
GivenTorrents(new List<QBittorrentTorrent>() { torrent });
|
||||
|
@ -738,6 +744,50 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_inactive_seedingtime_reached_and_not_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("uploading", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
|
||||
{
|
||||
|
@ -749,6 +799,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_fetch_details_twice()
|
||||
{
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework
|
|||
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
||||
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
||||
Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Tags;
|
||||
|
@ -43,5 +46,35 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
|||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_used_auto_tagging_tag_specification_tags()
|
||||
{
|
||||
var tags = Builder<Tag>
|
||||
.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.BuildList();
|
||||
Db.InsertMany(tags);
|
||||
|
||||
var autoTags = Builder<AutoTag>.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.With(x => x.Specifications = new List<IAutoTaggingSpecification>
|
||||
{
|
||||
new TagSpecification
|
||||
{
|
||||
Name = "Test",
|
||||
Value = tags[0].Id
|
||||
}
|
||||
})
|
||||
.BuildList();
|
||||
|
||||
Mocker.GetMock<IAutoTaggingRepository>().Setup(s => s.All())
|
||||
.Returns(autoTags);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class CustomFormatsFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Movie _movie;
|
||||
private MovieFile _movieFile;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
private List<CustomFormat> _customFormats;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_movie = Builder<Movie>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "South Park")
|
||||
.Build();
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.RenameMovies = true;
|
||||
|
||||
Mocker.GetMock<INamingConfigService>()
|
||||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
_movieFile = new MovieFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "RadarrTest" };
|
||||
|
||||
_customFormats = new List<CustomFormat>()
|
||||
{
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "INTERNAL",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
},
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "AMZN",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
},
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "NAME WITH SPACES",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
},
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "NotIncludedFormat",
|
||||
IncludeCustomFormatWhenRenaming = false
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats}", "INTERNAL AMZN NAME WITH SPACES")]
|
||||
public void should_replace_custom_formats(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat = format;
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats}", "")]
|
||||
public void should_replace_custom_formats_with_no_custom_formats(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat = format;
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile, customFormats: new List<CustomFormat>())
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats:-INTERNAL}", "AMZN NAME WITH SPACES")]
|
||||
[TestCase("{Custom Formats:-NAME WITH SPACES}", "INTERNAL AMZN")]
|
||||
[TestCase("{Custom Formats:-INTERNAL,NAME WITH SPACES}", "AMZN")]
|
||||
[TestCase("{Custom Formats:INTERNAL}", "INTERNAL")]
|
||||
[TestCase("{Custom Formats:NAME WITH SPACES}", "NAME WITH SPACES")]
|
||||
[TestCase("{Custom Formats:INTERNAL,NAME WITH SPACES}", "INTERNAL NAME WITH SPACES")]
|
||||
public void should_replace_custom_formats_with_filtered_names(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat = format;
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats:-}", "{Custom Formats:-}")]
|
||||
[TestCase("{Custom Formats:}", "{Custom Formats:}")]
|
||||
public void should_not_replace_custom_formats_due_to_invalid_token(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat = format;
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Format}", "")]
|
||||
[TestCase("{Custom Format:INTERNAL}", "INTERNAL")]
|
||||
[TestCase("{Custom Format:AMZN}", "AMZN")]
|
||||
[TestCase("{Custom Format:NAME WITH SPACES}", "NAME WITH SPACES")]
|
||||
[TestCase("{Custom Format:DOESNOTEXIST}", "")]
|
||||
[TestCase("{Custom Format:INTERNAL} - {Custom Format:AMZN}", "INTERNAL - AMZN")]
|
||||
[TestCase("{Custom Format:AMZN} - {Custom Format:INTERNAL}", "AMZN - INTERNAL")]
|
||||
public void should_replace_custom_format(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat = format;
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Format}", "")]
|
||||
[TestCase("{Custom Format:INTERNAL}", "")]
|
||||
[TestCase("{Custom Format:AMZN}", "")]
|
||||
public void should_replace_custom_format_with_no_custom_formats(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat = format;
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile, customFormats: new List<CustomFormat>())
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -84,7 +84,8 @@ namespace NzbDrone.Core.Annotations
|
|||
Device,
|
||||
TagSelect,
|
||||
RootFolder,
|
||||
QualityProfile
|
||||
QualityProfile,
|
||||
MovieTag
|
||||
}
|
||||
|
||||
public enum HiddenType
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
{
|
||||
public class TagSpecificationValidator : AbstractValidator<TagSpecification>
|
||||
{
|
||||
public TagSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
public class TagSpecification : AutoTaggingSpecificationBase
|
||||
{
|
||||
private static readonly TagSpecificationValidator Validator = new ();
|
||||
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Tag";
|
||||
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationTag", Type = FieldType.MovieTag)]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||
{
|
||||
return movie.Tags.Contains(Value);
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||
|
||||
public abstract NzbDroneValidationResult Validate();
|
||||
|
||||
public bool IsSatisfiedBy(CustomFormatInput input)
|
||||
public virtual bool IsSatisfiedBy(CustomFormatInput input)
|
||||
{
|
||||
var match = IsSatisfiedByWithoutNegate(input);
|
||||
|
||||
|
|
|
@ -30,6 +30,16 @@ namespace NzbDrone.Core.CustomFormats
|
|||
[FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))]
|
||||
public int Value { get; set; }
|
||||
|
||||
public override bool IsSatisfiedBy(CustomFormatInput input)
|
||||
{
|
||||
if (Negate)
|
||||
{
|
||||
return IsSatisfiedByWithNegate(input);
|
||||
}
|
||||
|
||||
return IsSatisfiedByWithoutNegate(input);
|
||||
}
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||
{
|
||||
var comparedLanguage = input.MovieInfo != null && input.Movie != null && Value == Language.Original.Id && input.Movie.MovieMetadata.Value.OriginalLanguage != Language.Unknown
|
||||
|
@ -39,6 +49,15 @@ namespace NzbDrone.Core.CustomFormats
|
|||
return input?.Languages?.Contains(comparedLanguage) ?? false;
|
||||
}
|
||||
|
||||
private bool IsSatisfiedByWithNegate(CustomFormatInput input)
|
||||
{
|
||||
var comparedLanguage = input.MovieInfo != null && input.Movie != null && Value == Language.Original.Id && input.Movie.MovieMetadata.Value.OriginalLanguage != Language.Unknown
|
||||
? input.Movie.MovieMetadata.Value.OriginalLanguage
|
||||
: (Language)Value;
|
||||
|
||||
return !input.Languages?.Contains(comparedLanguage) ?? false;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -16,12 +16,12 @@ namespace NzbDrone.Core.Datastore
|
|||
}
|
||||
|
||||
public CorruptDatabaseException(string message, Exception innerException, params object[] args)
|
||||
: base(message, innerException, args)
|
||||
: base(innerException, message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public CorruptDatabaseException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
: base(innerException, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -388,16 +388,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
}
|
||||
}
|
||||
|
||||
var minimumRetention = 60 * 24 * 14;
|
||||
|
||||
return new DownloadClientInfo
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
|
||||
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) },
|
||||
RemovesCompletedDownloads = (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles)
|
||||
RemovesCompletedDownloads = RemovesCompletedDownloads(config)
|
||||
};
|
||||
}
|
||||
|
||||
private bool RemovesCompletedDownloads(QBittorrentPreferences config)
|
||||
{
|
||||
var minimumRetention = 60 * 24 * 14; // 14 days in minutes
|
||||
return (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles);
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestConnection());
|
||||
|
@ -448,7 +452,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
|
||||
// Complain if qBittorrent is configured to remove torrents on max ratio
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles))
|
||||
if (RemovesCompletedDownloads(config))
|
||||
{
|
||||
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationRemovesAtRatioLimit"))
|
||||
{
|
||||
|
@ -626,7 +630,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
}
|
||||
}
|
||||
|
||||
if (HasReachedSeedingTimeLimit(torrent, config))
|
||||
if (HasReachedSeedingTimeLimit(torrent, config) || HasReachedInactiveSeedingTimeLimit(torrent, config))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -698,6 +702,26 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
return false;
|
||||
}
|
||||
|
||||
protected bool HasReachedInactiveSeedingTimeLimit(QBittorrentTorrent torrent, QBittorrentPreferences config)
|
||||
{
|
||||
long inactiveSeedingTimeLimit;
|
||||
|
||||
if (torrent.InactiveSeedingTimeLimit >= 0)
|
||||
{
|
||||
inactiveSeedingTimeLimit = torrent.InactiveSeedingTimeLimit * 60;
|
||||
}
|
||||
else if (torrent.InactiveSeedingTimeLimit == -2 && config.MaxInactiveSeedingTimeEnabled)
|
||||
{
|
||||
inactiveSeedingTimeLimit = config.MaxInactiveSeedingTime * 60;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() - torrent.LastActivity > inactiveSeedingTimeLimit;
|
||||
}
|
||||
|
||||
protected void FetchTorrentDetails(QBittorrentTorrent torrent)
|
||||
{
|
||||
var torrentProperties = Proxy.GetTorrentProperties(torrent.Hash, Settings);
|
||||
|
|
|
@ -28,6 +28,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
[JsonProperty(PropertyName = "max_seeding_time")]
|
||||
public long MaxSeedingTime { get; set; } // Get the global share time limit in minutes
|
||||
|
||||
[JsonProperty(PropertyName = "max_inactive_seeding_time_enabled")]
|
||||
public bool MaxInactiveSeedingTimeEnabled { get; set; } // True if share inactive time limit is enabled
|
||||
|
||||
[JsonProperty(PropertyName = "max_inactive_seeding_time")]
|
||||
public long MaxInactiveSeedingTime { get; set; } // Get the global share inactive time limit in minutes
|
||||
|
||||
[JsonProperty(PropertyName = "max_ratio_act")]
|
||||
public QBittorrentMaxRatioAction MaxRatioAction { get; set; } // Action performed when a torrent reaches the maximum share ratio.
|
||||
|
||||
|
|
|
@ -37,6 +37,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
|
||||
[JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited)
|
||||
public long SeedingTimeLimit { get; set; } = -2;
|
||||
|
||||
[JsonProperty(PropertyName = "inactive_seeding_time_limit")] // Per torrent inactive seeding time limit (-2 = use global, -1 = unlimited)
|
||||
public long InactiveSeedingTimeLimit { get; set; } = -2;
|
||||
|
||||
[JsonProperty(PropertyName = "last_activity")] // Timestamp in unix seconds when a chunk was last downloaded/uploaded
|
||||
public long LastActivity { get; set; }
|
||||
}
|
||||
|
||||
public class QBittorrentTorrentProperties
|
||||
|
|
|
@ -139,12 +139,14 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||
// Ignore torrents with an empty path
|
||||
if (torrent.Path.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Warn("Torrent '{0}' has an empty download path and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (torrent.Path.StartsWith("."))
|
||||
{
|
||||
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");
|
||||
_logger.Warn("Torrent '{0}' has a download path starting with '.' and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem();
|
||||
|
|
|
@ -34,6 +34,7 @@ namespace NzbDrone.Core.Download
|
|||
{
|
||||
{ Result.HasHttpServerError: true } => PredicateResult.True(),
|
||||
{ Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(),
|
||||
{ Exception: HttpException { Response.HasHttpServerError: true } } => PredicateResult.True(),
|
||||
_ => PredicateResult.False()
|
||||
},
|
||||
Delay = TimeSpan.FromSeconds(3),
|
||||
|
|
|
@ -156,13 +156,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
|||
|
||||
details.Add(new XElement("sorttitle", Parser.Parser.NormalizeTitle(metadataTitle)));
|
||||
|
||||
if (movie.MovieMetadata.Value.Ratings.Tmdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings.Imdb?.Votes > 0)
|
||||
if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings?.Imdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0)
|
||||
{
|
||||
var setRating = new XElement("ratings");
|
||||
|
||||
var defaultRatingSet = false;
|
||||
|
||||
if (movie.MovieMetadata.Value.Ratings.Imdb?.Votes > 0)
|
||||
if (movie.MovieMetadata.Value.Ratings?.Imdb?.Votes > 0)
|
||||
{
|
||||
var setRateImdb = new XElement("rating", new XAttribute("name", "imdb"), new XAttribute("max", "10"), new XAttribute("default", "true"));
|
||||
setRateImdb.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.Imdb.Value));
|
||||
|
@ -172,18 +172,32 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
|||
setRating.Add(setRateImdb);
|
||||
}
|
||||
|
||||
if (movie.MovieMetadata.Value.Ratings.Tmdb?.Votes > 0)
|
||||
if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0)
|
||||
{
|
||||
var setRatethemoviedb = new XElement("rating", new XAttribute("name", "themoviedb"), new XAttribute("max", "10"));
|
||||
setRatethemoviedb.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.Tmdb.Value));
|
||||
setRatethemoviedb.Add(new XElement("votes", movie.MovieMetadata.Value.Ratings.Tmdb.Votes));
|
||||
var setRateTheMovieDb = new XElement("rating", new XAttribute("name", "themoviedb"), new XAttribute("max", "10"));
|
||||
setRateTheMovieDb.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.Tmdb.Value));
|
||||
setRateTheMovieDb.Add(new XElement("votes", movie.MovieMetadata.Value.Ratings.Tmdb.Votes));
|
||||
|
||||
if (!defaultRatingSet)
|
||||
{
|
||||
setRatethemoviedb.SetAttributeValue("default", "true");
|
||||
defaultRatingSet = true;
|
||||
setRateTheMovieDb.SetAttributeValue("default", "true");
|
||||
}
|
||||
|
||||
setRating.Add(setRatethemoviedb);
|
||||
setRating.Add(setRateTheMovieDb);
|
||||
}
|
||||
|
||||
if (movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0)
|
||||
{
|
||||
var setRateRottenTomatoes = new XElement("rating", new XAttribute("name", "tomatometerallcritics"), new XAttribute("max", "100"));
|
||||
setRateRottenTomatoes.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.RottenTomatoes.Value));
|
||||
|
||||
if (!defaultRatingSet)
|
||||
{
|
||||
setRateRottenTomatoes.SetAttributeValue("default", "true");
|
||||
}
|
||||
|
||||
setRating.Add(setRateRottenTomatoes);
|
||||
}
|
||||
|
||||
details.Add(setRating);
|
||||
|
@ -194,6 +208,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
|||
details.Add(new XElement("rating", movie.MovieMetadata.Value.Ratings.Tmdb.Value));
|
||||
}
|
||||
|
||||
if (movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0)
|
||||
{
|
||||
details.Add(new XElement("criticrating", movie.MovieMetadata.Value.Ratings.RottenTomatoes.Value));
|
||||
}
|
||||
|
||||
details.Add(new XElement("userrating"));
|
||||
|
||||
details.Add(new XElement("top250"));
|
||||
|
|
|
@ -3,6 +3,8 @@ using System.Data;
|
|||
using System.Linq;
|
||||
using Dapper;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
@ -10,17 +12,24 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
|||
public class CleanupUnusedTags : IHousekeepingTask
|
||||
{
|
||||
private readonly IMainDatabase _database;
|
||||
private readonly IAutoTaggingRepository _autoTaggingRepository;
|
||||
|
||||
public CleanupUnusedTags(IMainDatabase database)
|
||||
public CleanupUnusedTags(IMainDatabase database, IAutoTaggingRepository autoTaggingRepository)
|
||||
{
|
||||
_database = database;
|
||||
_autoTaggingRepository = autoTaggingRepository;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using var mapper = _database.OpenConnection();
|
||||
var usedTags = new[] { "Movies", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" }
|
||||
var usedTags = new[]
|
||||
{
|
||||
"Movies", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers",
|
||||
"AutoTagging", "DownloadClients"
|
||||
}
|
||||
.SelectMany(v => GetUsedTags(v, mapper))
|
||||
.Concat(GetAutoTaggingTagSpecificationTags(mapper))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
|
@ -45,10 +54,31 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
|||
|
||||
private int[] GetUsedTags(string table, IDbConnection mapper)
|
||||
{
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
|
||||
return mapper
|
||||
.Query<List<int>>(
|
||||
$"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
|
||||
.SelectMany(x => x)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private List<int> GetAutoTaggingTagSpecificationTags(IDbConnection mapper)
|
||||
{
|
||||
var tags = new List<int>();
|
||||
var autoTags = _autoTaggingRepository.All();
|
||||
|
||||
foreach (var autoTag in autoTags)
|
||||
{
|
||||
foreach (var specification in autoTag.Specifications)
|
||||
{
|
||||
if (specification is TagSpecification tagSpec)
|
||||
{
|
||||
tags.Add(tagSpec.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ namespace NzbDrone.Core.ImportLists.TMDb.Popular
|
|||
var certification = Settings.FilterCriteria.Certification;
|
||||
var includeGenreIds = Settings.FilterCriteria.IncludeGenreIds;
|
||||
var excludeGenreIds = Settings.FilterCriteria.ExcludeGenreIds;
|
||||
var includeCompanyIds = Settings.FilterCriteria.IncludeCompanyIds;
|
||||
var excludeCompanyIds = Settings.FilterCriteria.ExcludeCompanyIds;
|
||||
var languageCode = (TMDbLanguageCodes)Settings.FilterCriteria.LanguageCode;
|
||||
|
||||
var todaysDate = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
|
@ -92,6 +94,16 @@ namespace NzbDrone.Core.ImportLists.TMDb.Popular
|
|||
requestBuilder.AddQueryParam("without_genres", excludeGenreIds);
|
||||
}
|
||||
|
||||
if (includeCompanyIds.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
requestBuilder.AddQueryParam("with_companies", includeCompanyIds);
|
||||
}
|
||||
|
||||
if (excludeCompanyIds.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
requestBuilder.AddQueryParam("without_companies", excludeCompanyIds);
|
||||
}
|
||||
|
||||
requestBuilder
|
||||
.AddQueryParam("with_original_language", languageCode)
|
||||
.Accept(HttpAccept.Json);
|
||||
|
|
|
@ -38,6 +38,18 @@ namespace NzbDrone.Core.ImportLists.TMDb
|
|||
.Matches(@"^\d+([,|]\d+)*$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.ExcludeGenreIds.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Genre Ids must be comma (,) or pipe (|) separated number ids");
|
||||
|
||||
// CSV of numbers
|
||||
RuleFor(c => c.IncludeCompanyIds)
|
||||
.Matches(@"^\d+([,|]\d+)*$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.IncludeCompanyIds.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Company Ids must be comma (,) or pipe (|) separated number ids");
|
||||
|
||||
// CSV of numbers
|
||||
RuleFor(c => c.ExcludeCompanyIds)
|
||||
.Matches(@"^\d+([,|]\d+)*$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.ExcludeCompanyIds.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Company Ids must be comma (,) or pipe (|) separated number ids");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,8 +60,10 @@ namespace NzbDrone.Core.ImportLists.TMDb
|
|||
MinVoteAverage = "5";
|
||||
MinVotes = "1";
|
||||
LanguageCode = (int)TMDbLanguageCodes.en;
|
||||
ExcludeGenreIds = "";
|
||||
IncludeGenreIds = "";
|
||||
ExcludeGenreIds = "";
|
||||
IncludeCompanyIds = "";
|
||||
ExcludeCompanyIds = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Minimum Vote Average", HelpText = "Filter movies by votes (0.0-10.0)")]
|
||||
|
@ -67,7 +81,13 @@ namespace NzbDrone.Core.ImportLists.TMDb
|
|||
[FieldDefinition(5, Label = "Exclude Genre Ids", HelpText = "Filter movies by TMDb Genre Ids (Comma Separated)")]
|
||||
public string ExcludeGenreIds { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Original Language", Type = FieldType.Select, SelectOptions = typeof(TMDbLanguageCodes), HelpText = "Filter by Language")]
|
||||
[FieldDefinition(6, Label = "Include Company Ids", HelpText = "Filter movies by TMDb Company Ids (Comma Separated)")]
|
||||
public string IncludeCompanyIds { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Exclude Company Ids", HelpText = "Filter movies by TMDb Company Ids (Comma Separated)")]
|
||||
public string ExcludeCompanyIds { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "Original Language", Type = FieldType.Select, SelectOptions = typeof(TMDbLanguageCodes), HelpText = "Filter by Language")]
|
||||
public int LanguageCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
|
||||
var reports = batch.SelectMany(x => x).ToList();
|
||||
|
||||
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
|
||||
_logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
|
||||
|
||||
// Update the last search time for movie if at least 1 indexer was searched.
|
||||
if (indexers.Any())
|
||||
|
|
|
@ -46,27 +46,27 @@ namespace NzbDrone.Core.Indexers.FileList
|
|||
[FieldDefinition(1, Label = "Passkey", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
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(2, 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.")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Categories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), HelpText = "Categories for use in search and feeds. If unspecified, all options are used.")]
|
||||
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), HelpText = "Categories for use in search and feeds. If unspecified, all options are used.")]
|
||||
public IEnumerable<int> Categories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
[FieldDefinition(4, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(7)]
|
||||
[FieldDefinition(5)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
|
||||
|
||||
[FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(6, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(8, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -53,21 +53,21 @@ namespace NzbDrone.Core.Indexers.HDBits
|
|||
[FieldDefinition(5, Label = "Mediums", Type = FieldType.Select, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "If unspecified, all options are used.")]
|
||||
public IEnumerable<int> Mediums { get; set; }
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
[FieldDefinition(6, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(8, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(9)]
|
||||
[FieldDefinition(7)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new ();
|
||||
|
||||
[FieldDefinition(10, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(9, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(10, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -41,21 +41,21 @@ 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 ...)")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
[FieldDefinition(1, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
[FieldDefinition(2)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(3, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace NzbDrone.Core.Indexers
|
|||
public abstract class IndexerBase<TSettings> : IIndexer
|
||||
where TSettings : IIndexerSettings, new()
|
||||
{
|
||||
private static readonly Regex MultiRegex = new (@"\b(?<multi>multi)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex MultiRegex = new (@"[_. ](?<multi>multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
protected readonly IIndexerStatusService _indexerStatusService;
|
||||
protected readonly IConfigService _configService;
|
||||
|
|
|
@ -65,23 +65,19 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
|
||||
public string ApiPath { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Drop down list; at least one category must be selected.")]
|
||||
public IEnumerable<int> Categories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||
[FieldDefinition(4, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||
public string AdditionalParameters { get; set; }
|
||||
|
||||
[FieldDefinition(6,
|
||||
Label = "Remove year from search string",
|
||||
HelpText = "Should Radarr remove the year after the title when searching this indexer?",
|
||||
Advanced = true,
|
||||
Type = FieldType.Checkbox)]
|
||||
[FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Remove year from search string", HelpText = "Should Radarr remove the year after the title when searching this indexer?", Type = FieldType.Checkbox, Advanced = true)]
|
||||
public bool RemoveYear { get; set; }
|
||||
|
||||
// Field 8 is used by TorznabSettings MinimumSeeders
|
||||
|
|
|
@ -36,24 +36,24 @@ namespace NzbDrone.Core.Indexers.Nyaa
|
|||
[FieldDefinition(0, Label = "Website URL")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
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(1, 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.")]
|
||||
public string AdditionalParameters { get; set; }
|
||||
|
||||
[FieldDefinition(3, 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)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(5)]
|
||||
[FieldDefinition(3)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -35,27 +35,27 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
|||
[FieldDefinition(0, Label = "URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your cookie will be sent to that host.")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "APIUser", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)]
|
||||
[FieldDefinition(1, Label = "API User", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)]
|
||||
public string APIUser { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "APIKey", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
[FieldDefinition(2, Label = "API Key", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string APIKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
[FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(6)]
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers
|
|||
public class RssSyncCommand : Command
|
||||
{
|
||||
public override bool SendUpdatesToClient => true;
|
||||
|
||||
public override bool IsLongRunning => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,21 +39,21 @@ namespace NzbDrone.Core.Indexers.TorrentPotato
|
|||
[FieldDefinition(2, Label = "Passkey", HelpText = "The password you use at your Indexer.", Privacy = PrivacyLevel.Password)]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(4, 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)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(6)]
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -40,21 +40,21 @@ namespace NzbDrone.Core.Indexers.TorrentRss
|
|||
[FieldDefinition(2, Type = FieldType.Checkbox, Label = "Allow Zero Size", HelpText = "Enabling this will allow you to use feeds that don't specify release size, but be careful, size related checks will not be performed.")]
|
||||
public bool AllowZeroSize { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(4, 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)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(6)]
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -58,12 +58,12 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||
[FieldDefinition(9)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new ();
|
||||
|
||||
[FieldDefinition(10, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
[FieldDefinition(11, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
[FieldDefinition(10, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
[FieldDefinition(11, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
|
||||
public IEnumerable<int> RequiredFlags { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -353,7 +353,6 @@
|
|||
"IncludeUnmonitored": "تضمين غير مراقب",
|
||||
"IncludeRecommendationsHelpText": "قم بتضمين أفلام {appName} الموصى بها في عرض الاكتشاف",
|
||||
"IncludeRadarrRecommendations": "تضمين توصيات الرادار",
|
||||
"IncludeHealthWarningsHelpText": "قم بتضمين التحذيرات الصحية",
|
||||
"IncludeCustomFormatWhenRenamingHelpText": "تضمين في تنسيق إعادة تسمية {تنسيقات مخصصة}",
|
||||
"IncludeCustomFormatWhenRenaming": "قم بتضمين تنسيق مخصص عند إعادة التسمية",
|
||||
"InCinemasMsg": "الفيلم في دور السينما",
|
||||
|
|
|
@ -813,7 +813,6 @@
|
|||
"ICalFeedHelpText": "Копирайте този URL адрес на вашите клиенти или кликнете, за да се абонирате, ако браузърът ви поддържа webcal",
|
||||
"Imported": "Внос",
|
||||
"IllRestartLater": "Ще рестартирам по-късно",
|
||||
"IncludeHealthWarningsHelpText": "Включете здравни предупреждения",
|
||||
"Medium": "Среден",
|
||||
"MinAvailability": "Минимална наличност",
|
||||
"MinimumAge": "Минимална възраст",
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"LastWriteTime": "La darrera hora d'escriptura",
|
||||
"LocalPath": "Camí local",
|
||||
"Logs": "Registres",
|
||||
"MinutesNinety": "90 minuts: {0}",
|
||||
"MinutesNinety": "90 minuts: {ninety}",
|
||||
"NoListRecommendations": "No s'han trobat elements de llista ni recomanacions; per a començar, podeu afegir una pel·lícula nova, importar-ne algunes existents o afegir una llista.",
|
||||
"NotAvailable": "No disponible",
|
||||
"PreferAndUpgrade": "Marca preferit i actualitza",
|
||||
|
@ -287,7 +287,7 @@
|
|||
"MovieChat": "Xat de pel·lícula",
|
||||
"MovieDetailsNextMovie": "Detalls de la pel·lícula: propera pel·lícula",
|
||||
"MovieInvalidFormat": "Pel·lícula: Format no vàlid",
|
||||
"NegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {0} coincideix.",
|
||||
"NegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.",
|
||||
"NetCore": ".NET",
|
||||
"NoLeaveIt": "No, deixa-ho",
|
||||
"TotalMovies": "Total de pel·lícules",
|
||||
|
@ -410,8 +410,8 @@
|
|||
"Custom": "Personalitzat",
|
||||
"CustomFilters": "Filtres personalitzats",
|
||||
"CustomFormats": "Formats personalitzats",
|
||||
"CustomFormatUnknownCondition": "Condició de format personalitzada desconeguda '{0}'",
|
||||
"CustomFormatUnknownConditionOption": "Opció desconeguda '{0}' per a la condició '{1}'",
|
||||
"CustomFormatUnknownCondition": "Condició de format personalitzada desconeguda '{implementation}'",
|
||||
"CustomFormatUnknownConditionOption": "Opció desconeguda '{key}' per a la condició '{implementation}'",
|
||||
"UpgradeUntilCustomFormatScoreMovieHelpText": "Un cop s'arribi a aquesta puntuació de format personalitzat, {appName} ja no baixarà pel·lícules",
|
||||
"UpgradeUntilMovieHelpText": "Un cop s'assoleixi aquesta qualitat, {appName} ja no baixarà pel·lícules",
|
||||
"Database": "Base de dades",
|
||||
|
@ -448,7 +448,7 @@
|
|||
"DoNotPrefer": "No ho prefereixo",
|
||||
"DoNotUpgradeAutomatically": "No actualitzeu automàticament",
|
||||
"DownloadClientCheckDownloadingToRoot": "El client de baixada {downloadClientName} col·loca les baixades a la carpeta arrel {path}. No s'hauria de baixar a una carpeta arrel.",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "No es pot comunicar amb {downloadClientName}.",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "No es pot comunicar amb {downloadClientName}. {errorMessage}",
|
||||
"DownloadClientsSettingsSummary": "Descàrrega de clients, gestió de descàrregues i mapes de camins remots",
|
||||
"DownloadClientUnavailable": "El client de descàrrega no està disponible",
|
||||
"Downloaded": "S'ha baixat",
|
||||
|
@ -531,7 +531,6 @@
|
|||
"InCinemasMsg": "Estrena de pel·lícula",
|
||||
"IncludeCustomFormatWhenRenaming": "Inclou el format personalitzat en canviar el nom",
|
||||
"IncludeCustomFormatWhenRenamingHelpText": "Inclou en {Custom Formats} el format de canvi de nom",
|
||||
"IncludeHealthWarningsHelpText": "Inclou advertències de salut",
|
||||
"IncludeRecommendationsHelpText": "Inclou les pel·lícules recomanades per {appName} a la vista de descobriment",
|
||||
"IndexerJackettAll": "Indexadors que utilitzen el punt final \"tot\" no compatible amb Jackett: {indexerNames}",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors durant més de 6 hores",
|
||||
|
@ -559,7 +558,7 @@
|
|||
"Links": "Enllaços",
|
||||
"ImportListExclusions": "Llista d'exclusions",
|
||||
"ImportLists": "Llistes",
|
||||
"ImportListsSettingsSummary": "Importa llistes, exclusions de llista",
|
||||
"ImportListsSettingsSummary": "Importa des d'una altra instància {appName} o llistes de Trakt i gestiona les exclusions de llistes",
|
||||
"ListSyncLevelHelpText": "Les pel·lícules de la biblioteca es gestionaran en funció de la vostra selecció si cauen o no apareixen a les vostres llistes",
|
||||
"ListSyncLevelHelpTextWarning": "Els fitxers de pel·lícules se suprimiran permanentment; això pot provocar que esborraràs la teva biblioteca si les teves llistes estan buides",
|
||||
"ListTagsHelpText": "Els elements de la llista d'etiquetes s'afegiran amb",
|
||||
|
@ -598,8 +597,8 @@
|
|||
"MinimumFreeSpace": "Espai lliure mínim",
|
||||
"MinimumFreeSpaceHelpText": "Eviteu la importació si quedara menys d'aquesta quantitat d'espai disponible en disc",
|
||||
"Minutes": "Minuts",
|
||||
"MinutesHundredTwenty": "120 minuts: {0}",
|
||||
"MinutesSixty": "60 minuts: {0}",
|
||||
"MinutesHundredTwenty": "120 minuts: {hundredTwenty}",
|
||||
"MinutesSixty": "60 minuts: {sixty}",
|
||||
"Missing": "Absents",
|
||||
"MonitorCollection": "Monitora col·leccions",
|
||||
"MonitoredCollectionHelpText": "Monitora per a afegir automàticament pel·lícules d'aquesta col·lecció a la biblioteca",
|
||||
|
@ -716,7 +715,7 @@
|
|||
"RefreshLists": "Actualitza llistes",
|
||||
"RefreshMonitoredIntervalHelpText": "Amb quina freqüència s'actualitzen les baixades monitorades dels clients de descàrrega, mínim 1 minut",
|
||||
"RefreshMovie": "Actualitza pel·lícula",
|
||||
"RegularExpressionsCanBeTested": "Es poden provar expressions regulars ",
|
||||
"RegularExpressionsCanBeTested": "Les expressions regulars es poden provar [aquí](http://regexstorm.net/tester).",
|
||||
"RejectionCount": "Recompte de rebuigs",
|
||||
"RelativePath": "Camí relatiu",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "La branca {0} no és una branca de llançament de {appName} vàlida, no rebreu actualitzacions",
|
||||
|
@ -877,7 +876,7 @@
|
|||
"Tomorrow": "Demà",
|
||||
"TorrentDelay": "Retard del torrent",
|
||||
"TorrentDelayHelpText": "Retard en minuts per a esperar abans de capturar un torrent",
|
||||
"TorrentDelayTime": "Retard del torrent: {0}",
|
||||
"TorrentDelayTime": "Retard del torrent: {torrentDelay}",
|
||||
"Torrents": "Torrents",
|
||||
"TorrentsDisabled": "Torrents desactivats",
|
||||
"TotalFileSize": "Mida total del fitxer",
|
||||
|
@ -917,7 +916,7 @@
|
|||
"UpdateCheckStartupNotWritableMessage": "L'actualització no es pot instal·lar perquè la carpeta d'inici '{startupFolder}' no té permisos d'escriptura per a l'usuari '{userName}'.",
|
||||
"UpdateSelected": "Actualització seleccionada",
|
||||
"UpdateCheckStartupTranslocationMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici \"{startupFolder}\" es troba en una carpeta de translocació d'aplicacions.",
|
||||
"UpdateCheckUINotWritableMessage": "L'actualització no es pot instal·lar perquè la carpeta UI '{startupFolder}' no té permisos d'escriptura per a l'usuari '{userName}'.",
|
||||
"UpdateCheckUINotWritableMessage": "L'actualització no es pot instal·lar perquè la carpeta UI '{uiFolder}' no té permisos d'escriptura per a l'usuari '{userName}'.",
|
||||
"UpdateMechanismHelpText": "Utilitzeu l'actualitzador integrat de {appName} o un script",
|
||||
"UpdateScriptPathHelpText": "Camí a un script personalitzat que pren un paquet d'actualització i gestiona la resta del procés d'actualització",
|
||||
"UpgradesAllowedHelpText": "Si les qualitats estan desactivades no s'actualitzaran",
|
||||
|
@ -929,7 +928,7 @@
|
|||
"UseHardlinksInsteadOfCopy": "Utilitzeu enllaços durs en lloc de copiar",
|
||||
"Usenet": "Usenet",
|
||||
"UsenetDelayHelpText": "Retard en minuts per esperar abans de capturar una versió d'Usenet",
|
||||
"UsenetDelayTime": "Retard d'Usenet: {0}",
|
||||
"UsenetDelayTime": "Retard d'Usenet: {usenetDelay}",
|
||||
"UsenetDisabled": "Usenet desactivat",
|
||||
"UseProxy": "Utilitzeu el servidor intermediari",
|
||||
"Username": "Nom d'usuari",
|
||||
|
@ -994,7 +993,7 @@
|
|||
"NoIssuesWithYourConfiguration": "No hi ha cap problema amb la configuració",
|
||||
"HiddenClickToShow": "Amagat, feu clic per a mostrar",
|
||||
"Max": "Màx",
|
||||
"RequiredHelpText": "La condició {0} ha de coincidir perquè s'apliqui el format personalitzat. En cas contrari, n'hi ha prou amb una única coincidència de {1}.",
|
||||
"RequiredHelpText": "La condició {implementationName} ha de coincidir perquè s'apliqui el format personalitzat. En cas contrari, n'hi ha prou amb una única coincidència de {implementationName}.",
|
||||
"ShowTitle": "Mostra el títol",
|
||||
"Ignored": "Ignorat",
|
||||
"IgnoredAddresses": "Adreces ignorades",
|
||||
|
@ -1094,8 +1093,8 @@
|
|||
"ApplyTagsHelpTextHowToApplyDownloadClients": "Com aplicar etiquetes als clients de baixada seleccionats",
|
||||
"ApplyTagsHelpTextHowToApplyImportLists": "Com aplicar etiquetes a les llistes d'importació seleccionades",
|
||||
"MoveAutomatically": "Mou automàticament",
|
||||
"AutoTaggingNegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {0} coincideix.",
|
||||
"AutoTaggingRequiredHelpText": "La condició {0} ha de coincidir perquè s'apliqui el format personalitzat. En cas contrari, n'hi ha prou amb una única coincidència de {0}.",
|
||||
"AutoTaggingNegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.",
|
||||
"AutoTaggingRequiredHelpText": "La condició {implementationName} ha de coincidir perquè s'apliqui el format personalitzat. En cas contrari, n'hi ha prou amb una única coincidència de {implementationName}.",
|
||||
"DeleteAutoTagHelpText": "Esteu segur que voleu suprimir l'etiqueta '{name}'?",
|
||||
"DeleteRootFolderMessageText": "Esteu segur que voleu suprimir la carpeta arrel '{path}'?",
|
||||
"AddConnection": "Afegeix una connexió",
|
||||
|
@ -1113,7 +1112,7 @@
|
|||
"GrabId": "Captura ID",
|
||||
"OrganizeNothingToRename": "Èxit! La feina està acabada, no hi ha fitxers per a canviar el nom.",
|
||||
"OrganizeLoadError": "S'ha produït un error en carregar les previsualitzacions",
|
||||
"BlocklistReleaseHelpText": "Impedeix que {appName} torni a capturar aquesta versió automàticament",
|
||||
"BlocklistReleaseHelpText": "Impedeix que {appName} baixi aquesta versió mitjançant RSS o cerca automàtica",
|
||||
"ConnectionLostReconnect": "{appName} intentarà connectar-se automàticament, o podeu fer clic a recarregar.",
|
||||
"ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per a restaurar la funcionalitat.",
|
||||
"DelayingDownloadUntil": "S'està retardant la baixada fins al {date} a les {time}",
|
||||
|
@ -1127,7 +1126,7 @@
|
|||
"TablePageSize": "Mida de la pàgina",
|
||||
"TablePageSizeHelpText": "Nombre d'elements per mostrar a cada pàgina",
|
||||
"MovieFileDeletedTooltip": "S'ha suprimit el fitxer de pel·lícula",
|
||||
"RetryingDownloadOn": "S'està retardant la baixada fins a les {0} a les {1}",
|
||||
"RetryingDownloadOn": "S'està retardant la baixada fins al {date} a les {time}",
|
||||
"FormatAgeHours": "hores",
|
||||
"FormatAgeMinute": "minut",
|
||||
"FormatAgeMinutes": "minuts",
|
||||
|
@ -1202,7 +1201,7 @@
|
|||
"FailedToUpdateSettings": "No s'ha pogut actualitzar la configuració",
|
||||
"InteractiveImportNoImportMode": "S'ha de seleccionar un mode d'importació",
|
||||
"InteractiveImportNoMovie": "S'ha de triar una pel·lícula triar per a cada fitxer seleccionat",
|
||||
"InteractiveSearchResultsFailedErrorMessage": "La cerca ha fallat per {missatge}. Actualitza la informació de la pel·lícula i verifica que hi hagi la informació necessària abans de tornar a cercar.",
|
||||
"InteractiveSearchResultsFailedErrorMessage": "La cerca ha fallat per {message}. Actualitza la informació de la pel·lícula i verifica que hi hagi la informació necessària abans de tornar a cercar.",
|
||||
"LogFilesLocation": "Els fitxers de registre es troben a: {location}",
|
||||
"ManageDownloadClients": "Gestiona els clients de descàrrega",
|
||||
"MovieGrabbedHistoryTooltip": "Pel·lícula captura de {indexer} i enviada a {downloadClient}",
|
||||
|
@ -1211,7 +1210,7 @@
|
|||
"FullColorEventsHelpText": "Estil alterat per a pintar tot l'esdeveniment amb el color d'estat, en lloc de només la vora esquerra. No s'aplica a l'Agenda",
|
||||
"InteractiveImportNoFilesFound": "No s'ha trobat cap fitxer de vídeo a la carpeta seleccionada",
|
||||
"InteractiveImportNoLanguage": "S'ha de triar l'idioma per a cada fitxer seleccionat",
|
||||
"ListWillRefreshEveryInterval": "La llista s'actualitzarà cada {0}",
|
||||
"ListWillRefreshEveryInterval": "La llista s'actualitzarà cada {refreshInterval}",
|
||||
"MovieMatchType": "Tipus de concordança de pel·lícula",
|
||||
"NoDownloadClientsFound": "No s'han trobat clients de baixada",
|
||||
"NoIndexersFound": "No s'han trobat indexadors",
|
||||
|
@ -1289,5 +1288,54 @@
|
|||
"Rejections": "Rebutjats",
|
||||
"NotificationsPushoverSettingsExpire": "Venciment",
|
||||
"NotificationsEmailSettingsServer": "Servidor",
|
||||
"NotificationsSettingsWebhookMethod": "Mètode"
|
||||
"NotificationsSettingsWebhookMethod": "Mètode",
|
||||
"BlocklistAndSearch": "Llista de bloqueig i cerca",
|
||||
"BlocklistAndSearchHint": "Comença una cerca per reemplaçar després d'haver bloquejat",
|
||||
"AddDelayProfileError": "No s'ha pogut afegir un perfil realentit, torna-ho a probar",
|
||||
"DoNotBlocklistHint": "Elimina sense afegir a la llista de bloqueig",
|
||||
"DoNotBlocklist": "No afegiu a la llista de bloqueig",
|
||||
"BlackholeWatchFolder": "Monitora la carpeta",
|
||||
"BlackholeFolderHelpText": "Carpeta on {appName} emmagatzemarà els fitxers {extension}",
|
||||
"BlocklistAndSearchMultipleHint": "Comença una cerca per reemplaçar després d'haver bloquejat",
|
||||
"BlackholeWatchFolderHelpText": "Carpeta des de la qual {appName} hauria d'importar les baixades finalitzades",
|
||||
"BlocklistMultipleOnlyHint": "Afegeix a la llista de bloqueig sense cercar substituts",
|
||||
"BlocklistOnly": "Sols afegir a la llista de bloqueig",
|
||||
"Category": "Categoria",
|
||||
"ChangeCategoryHint": "Canvia la baixada a la \"Categoria post-importació\" des del client de descàrrega",
|
||||
"Directory": "Directori",
|
||||
"Destination": "Destinació",
|
||||
"DownloadClientDelugeTorrentStateError": "Deluge està informant d'un error",
|
||||
"ClickToChangeIndexerFlags": "Feu clic per canviar els indicadors de l'indexador",
|
||||
"ConditionUsingRegularExpressions": "Aquesta condició coincideix amb expressions regulars. Tingueu en compte que els caràcters `\\^$.|?*+()[{` tenen significats especials i cal escapar amb un `\\`",
|
||||
"Umask": "UMask",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2",
|
||||
"AddAutoTagError": "No es pot afegir una etiqueta automàtica nova, torneu-ho a provar.",
|
||||
"AddReleaseProfile": "Afegeix un perfil de llançament",
|
||||
"ConnectionSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {connectionName}, com ara {url}",
|
||||
"CustomFormatsSpecificationRegularExpression": "Expressió regular",
|
||||
"CustomFormatsSpecificationRegularExpressionHelpText": "El format personalitzat RegEx no distingeix entre majúscules i minúscules",
|
||||
"AddListExclusion": "Afegeix una llista d'exclusió",
|
||||
"ChangeCategory": "Canvia categoria",
|
||||
"AutoTaggingLoadError": "No es pot carregar l'etiquetatge automàtic",
|
||||
"BlocklistOnlyHint": "Afegir a la llista de bloqueig sense cercar substituts",
|
||||
"ChangeCategoryMultipleHint": "Canvia les baixades a la \"Categoria post-importació\" des del client de descàrrega",
|
||||
"ChownGroup": "Canvia el grup propietari",
|
||||
"Clone": "Clona",
|
||||
"DelayMinutes": "{delay} Minuts",
|
||||
"DelayProfileMovieTagsHelpText": "S'aplica a pel·lícules amb almenys una etiqueta coincident",
|
||||
"DelayProfileProtocol": "Protocol: {preferredProtocol}",
|
||||
"DeleteReleaseProfileMessageText": "Esteu segur que voleu suprimir el perfil de llançament '{name}'?",
|
||||
"DeleteSpecification": "Esborra especificació",
|
||||
"CutoffNotMet": "Tall no assolit",
|
||||
"CustomFilter": "Filtres personalitzats",
|
||||
"CustomFormatsSpecificationFlag": "Bandera",
|
||||
"Dash": "Guió",
|
||||
"DeleteSpecificationHelpText": "Esteu segur que voleu suprimir l'especificació '{name}'?",
|
||||
"DownloadClientDelugeSettingsDirectory": "Directori de baixada",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Directori al qual es mou quan s'hagi completat",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"DeleteReleaseProfile": "Suprimeix el perfil de llançament",
|
||||
"Donate": "Dona",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Afegeix un prefix a l'url json del Deluge, vegeu {url}"
|
||||
}
|
||||
|
|
|
@ -212,7 +212,6 @@
|
|||
"AddNewMessage": "Přidání nového filmu je snadné, stačí začít psát název filmu, který chcete přidat",
|
||||
"AddNewMovie": "Přidat nový film",
|
||||
"AddNewTmdbIdMessage": "Můžete také vyhledávat pomocí ID TMDb filmu. např. 'tmdb: 71663'",
|
||||
"IncludeHealthWarningsHelpText": "Zahrnout zdravotní varování",
|
||||
"AddListExclusionMovieHelpText": "Zabraňte přidávání filmů do {appName}u pomocí seznamů",
|
||||
"ImportHeader": "Chcete-li přidat filmy do {appName}u, importujte existující organizovanou knihovnu",
|
||||
"ImportListStatusCheckSingleClientMessage": "Seznamy nejsou k dispozici z důvodu selhání: {importListNames}",
|
||||
|
|
|
@ -94,11 +94,11 @@
|
|||
"Agenda": "Dagsorden",
|
||||
"Age": "Alder",
|
||||
"AddNewTmdbIdMessage": "Du kan også søge ved at bruge TMDB Id'et af en film. f.eks 'tmdb:71663'",
|
||||
"AddNewMovie": "Tilføj Ny Film",
|
||||
"AddNewMovie": "Tilføj ny film",
|
||||
"AddNewMessage": "Det er nemt at tilføje en ny film, bare start ved at skrive navnet på filmen du vil tilføje",
|
||||
"AddNew": "Tilføj Ny",
|
||||
"AddMovies": "Tilføj Film",
|
||||
"AddExclusion": "Tilføj Undtagelse",
|
||||
"AddExclusion": "Tilføj undtagelse",
|
||||
"Added": "Tilføjet",
|
||||
"Activity": "Aktivitet",
|
||||
"Actions": "Handlinger",
|
||||
|
@ -106,7 +106,7 @@
|
|||
"AnalyseVideoFiles": "Analyser videofiler",
|
||||
"System": "System",
|
||||
"AutoRedownloadFailedHelpText": "Søg automatisk efter og forsøg at downloade en anden udgivelse",
|
||||
"TestAllClients": "Test alle klienter",
|
||||
"TestAllClients": "Afprøv alle klienter",
|
||||
"UnableToLoadAltTitle": "Kan ikke indlæse alternative titler.",
|
||||
"WhatsNew": "Hvad er nyt?",
|
||||
"ProtocolHelpText": "Vælg hvilke protokol (r) du vil bruge, og hvilken der foretrækkes, når du vælger mellem ellers lige udgivelser",
|
||||
|
@ -198,7 +198,7 @@
|
|||
"ChangeHasNotBeenSavedYet": "Ændring er endnu ikke gemt",
|
||||
"CheckDownloadClientForDetails": "tjek download klient for flere detaljer",
|
||||
"CheckForFinishedDownloadsInterval": "Kontroller for færdige downloadsinterval",
|
||||
"AddIndexer": "Tilføj indeksør",
|
||||
"AddIndexer": "Tilføj indekser",
|
||||
"ChmodFolder": "chmod mappe",
|
||||
"ChmodFolderHelpText": "Oktal, anvendt under import / omdøbning til mediemapper og filer (uden udførelse af bits)",
|
||||
"ChmodFolderHelpTextWarning": "Dette fungerer kun, hvis den bruger, der kører {appName}, er ejeren af filen. Det er bedre at sikre, at downloadklienten indstiller tilladelserne korrekt.",
|
||||
|
@ -281,7 +281,6 @@
|
|||
"CancelPendingTask": "Er du sikker på, at du vil annullere denne afventende opgave?",
|
||||
"DeleteDownloadClient": "Slet Download Client",
|
||||
"ImportIncludeQuality": "Sørg for, at dine filer inkluderer kvaliteten i deres filnavne. f.eks. {0}",
|
||||
"IncludeHealthWarningsHelpText": "Inkluder sundhedsadvarsler",
|
||||
"Max": "Maks.",
|
||||
"Medium": "Medium",
|
||||
"MovieFilesTotaling": "Totale filmfiler",
|
||||
|
@ -327,8 +326,8 @@
|
|||
"ShowGenres": "Vis genrer",
|
||||
"Size": "Størrelse",
|
||||
"SuggestTranslationChange": "Foreslå ændring af oversættelsen",
|
||||
"TestAllIndexers": "Test alle indeksører",
|
||||
"TestAllLists": "Test alle lister",
|
||||
"TestAllIndexers": "Afprøv alle indeks",
|
||||
"TestAllLists": "Afprøv alle lister",
|
||||
"Queued": "I kø",
|
||||
"TMDb": "TMDb",
|
||||
"Tomorrow": "I morgen",
|
||||
|
@ -392,7 +391,7 @@
|
|||
"PortNumber": "Portnummer",
|
||||
"UpgradesAllowedHelpText": "Hvis deaktiveret, vil kvalitet ikke vil blive opgraderet",
|
||||
"PackageVersion": "Pakkeversion",
|
||||
"AddMovie": "Tilføj Film",
|
||||
"AddMovie": "Tilføj film",
|
||||
"AddRestriction": "Tilføj begrænsning",
|
||||
"Password": "Adgangskode",
|
||||
"Path": "Sti",
|
||||
|
@ -831,7 +830,7 @@
|
|||
"Style": "Stil",
|
||||
"SubfolderWillBeCreatedAutomaticallyInterp": "Undermappen '{0}' oprettes automatisk",
|
||||
"Sunday": "Søndag",
|
||||
"Table": "Bord",
|
||||
"Table": "Tabel",
|
||||
"TableOptions": "Tabelindstillinger",
|
||||
"TableOptionsColumnsMessage": "Vælg hvilke kolonner der er synlige og hvilken rækkefølge de vises i",
|
||||
"TagDetails": "Tagdetaljer - {0}",
|
||||
|
@ -839,8 +838,8 @@
|
|||
"Tags": "Mærker",
|
||||
"ICalTagsMoviesHelpText": "Gælder film med mindst et matchende tag",
|
||||
"Tasks": "Opgaver",
|
||||
"Test": "Prøve",
|
||||
"TestAll": "Test alle",
|
||||
"Test": "Afprøv",
|
||||
"TestAll": "Afprøv alle",
|
||||
"TheLogLevelDefault": "Logniveauet er som standard 'Info' og kan ændres i",
|
||||
"ThisCannotBeCancelled": "Dette kan ikke annulleres en gang startet uden genstart af {appName}.",
|
||||
"Title": "Titel",
|
||||
|
@ -901,13 +900,13 @@
|
|||
"UnmappedFolders": "Ikke-kortlagte mapper",
|
||||
"Unmonitored": "Uovervåget",
|
||||
"ICalIncludeUnmonitoredMoviesHelpText": "Inkluder ikke-overvågede film i iCal-feedet",
|
||||
"Unreleased": "Ikke tilgængelig",
|
||||
"Unreleased": "Ikke udgivet",
|
||||
"UnsavedChanges": "Ugemte ændringer",
|
||||
"UnselectAll": "Fravælg alle",
|
||||
"UpdateAll": "Opdater alle",
|
||||
"UpdateAutomaticallyHelpText": "Download og installer opdateringer automatisk. Du kan stadig installere fra System: Updates",
|
||||
"UpdateCheckStartupTranslocationMessage": "Kan ikke installere opdatering, fordi startmappen '{startupFolder}' er i en App Translocation-mappe.",
|
||||
"UpdateCheckUINotWritableMessage": "Kan ikke installere opdatering, fordi brugergrænsefladen \"{startupFolder}\" ikke kan skrives af brugeren \"{userName}\".",
|
||||
"UpdateCheckUINotWritableMessage": "Kan ikke installere opdatering, fordi '{userName}' ikke kan skrive til mappen for brugergrænseflade '{uiFolder}'.",
|
||||
"UpdateMechanismHelpText": "Brug {appName}s indbyggede opdatering eller et script",
|
||||
"UpdateScriptPathHelpText": "Sti til et brugerdefineret script, der tager en udpakket opdateringspakke og håndterer resten af opdateringsprocessen",
|
||||
"UpdateSelected": "Opdatering valgt",
|
||||
|
@ -993,5 +992,18 @@
|
|||
"ApplyTagsHelpTextAdd": "Tilføj: Føj tags til den eksisterende liste over tags",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Sådan anvendes tags på de valgte film",
|
||||
"DeleteSelectedDownloadClients": "Slet Download Client",
|
||||
"DeleteSelectedIndexers": "Slet Indexer"
|
||||
"DeleteSelectedIndexers": "Slet Indexer",
|
||||
"AnnouncedMsg": "Film er annonceret",
|
||||
"AddConditionImplementation": "Tilføj betingelse - {implementationName}",
|
||||
"AddConnection": "Tilføj forbindelse",
|
||||
"AddConnectionImplementation": "Tilføj forbindelse - {implementationName}",
|
||||
"AddImportList": "Tilføj importliste",
|
||||
"AddDownloadClientImplementation": "Tilføj downloadklient - {implementationName}",
|
||||
"AddImportListImplementation": "Tilføj importliste - {implementationName}",
|
||||
"ApplyChanges": "Anvend ændringer",
|
||||
"AddCondition": "Tilføj betingelse",
|
||||
"AllTitles": "All titler",
|
||||
"TablePageSize": "Sidestørrelse",
|
||||
"AddRootFolderError": "Kunne ikke tilføje rodmappe",
|
||||
"Unknown": "Ukendt"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue