mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-16 21:15:33 -04:00
Compare commits
67 Commits
v5.4.1.865
...
v5.5.0.873
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77e27bace | ||
|
|
8ea6d59d59 | ||
|
|
98668d0d25 | ||
|
|
649d57a234 | ||
|
|
dc7c8bf800 | ||
|
|
8d90c7678f | ||
|
|
02518e2116 | ||
|
|
3191a883dc | ||
|
|
31a714e6b3 | ||
|
|
f7ca0b8b06 | ||
|
|
56be9502af | ||
|
|
77381d3f72 | ||
|
|
198e6324e0 | ||
|
|
81c9537e5a | ||
|
|
d3cbb9be8d | ||
|
|
2e043c0cf7 | ||
|
|
ada33dc065 | ||
|
|
badb68b817 | ||
|
|
3bd1b3e972 | ||
|
|
6851de42a7 | ||
|
|
dd0b7c91f9 | ||
|
|
45ac69e2d9 | ||
|
|
9ccf0ecdb1 | ||
|
|
48a3467572 | ||
|
|
d0a10379f9 | ||
|
|
caab5e3614 | ||
|
|
4e47695f89 | ||
|
|
83bd4d0686 | ||
|
|
a75619c8ef | ||
|
|
28689006fb | ||
|
|
43b0589bea | ||
|
|
c4aad5800c | ||
|
|
0c998dac5c | ||
|
|
d41c0f0ab7 | ||
|
|
85b13b7e41 | ||
|
|
2a545a84b4 | ||
|
|
280083f4d7 | ||
|
|
d6dcae3d6a | ||
|
|
ebde4d3bc8 | ||
|
|
1ee30290ef | ||
|
|
d303eae7c6 | ||
|
|
584910514a | ||
|
|
a253181d7d | ||
|
|
7ea6918327 | ||
|
|
953d3ad3fb | ||
|
|
b9f4073514 | ||
|
|
86a17e7984 | ||
|
|
f38545f852 | ||
|
|
a7720e829d | ||
|
|
3a4eac4d59 | ||
|
|
04f792c55a | ||
|
|
ada326e4dd | ||
|
|
cae58d620b | ||
|
|
e84df18e8d | ||
|
|
a51ae70938 | ||
|
|
7cc04245ec | ||
|
|
2caf3c6725 | ||
|
|
41ff9352b9 | ||
|
|
d7b9b2ccb2 | ||
|
|
e90a50a3aa | ||
|
|
a0dd26c353 | ||
|
|
2286055d6a | ||
|
|
0a5a4e0a6f | ||
|
|
619c38c493 | ||
|
|
0b8694c627 | ||
|
|
e2793e56e9 | ||
|
|
68f61da321 |
13
.devcontainer/Radarr.code-workspace
Normal file
13
.devcontainer/Radarr.code-workspace
Normal file
@@ -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": {}
|
||||
}
|
||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -126,6 +126,7 @@ coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
.vscode/tasks.json
vendored
Normal file
44
.vscode/tasks.json
vendored
Normal file
@@ -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.1'
|
||||
majorVersion: '5.5.0'
|
||||
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}
|
||||
/>
|
||||
|
||||
53
frontend/src/Components/Form/MovieTagInput.tsx
Normal file
53
frontend/src/Components/Form/MovieTagInput.tsx
Normal file
@@ -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"
|
||||
}
|
||||
|
||||
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
72
frontend/src/Movie/Index/MovieIndexSearchMenuItem.tsx
Normal file
72
frontend/src/Movie/Index/MovieIndexSearchMenuItem.tsx
Normal file
@@ -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',
|
||||
|
||||
@@ -120,7 +120,8 @@ const editionTokens = [
|
||||
];
|
||||
|
||||
const customFormatTokens = [
|
||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' }
|
||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' },
|
||||
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
|
||||
];
|
||||
|
||||
const originalTokens = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
23
frontend/src/Store/Selectors/createMultiMoviesSelector.ts
Normal file
23
frontend/src/Store/Selectors/createMultiMoviesSelector.ts
Normal file
@@ -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;
|
||||
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
@@ -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;
|
||||
}
|
||||
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
@@ -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;
|
||||
49
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal file
49
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
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';
|
||||
|
||||
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> - {sortedMovies.map((m) => m.title).join(', ')}</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;
|
||||
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
@@ -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>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#595959" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Radarr" />
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#595959" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Radarr (Preview)" />
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"" }, ")]
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Security;
|
||||
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;
|
||||
@@ -28,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;
|
||||
@@ -41,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)
|
||||
@@ -246,7 +254,27 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
private bool HasRoutableIPv4Address()
|
||||
{
|
||||
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||
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)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -269,10 +297,10 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
}
|
||||
catch
|
||||
{
|
||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
||||
useIPv6 = false;
|
||||
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Title.2019.1080p.AMZN.WEB-Rip.DDP.5.1.HEVC", null)]
|
||||
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English] [Data Lass]", null)]
|
||||
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English]-DataLass", "DataLass")]
|
||||
public void should_parse_expected_release_group(string title, string expected)
|
||||
[TestCase("Movie Name (2017) (Showtime) (1080p.BD.DD5.1.x265-TheSickle[TAoE])", "TheSickle")]
|
||||
public void should_parse_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).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));
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
@@ -101,11 +103,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings)
|
||||
{
|
||||
var options = new
|
||||
dynamic options = new ExpandoObject();
|
||||
|
||||
options.add_paused = settings.AddPaused;
|
||||
options.remove_at_ratio = false;
|
||||
|
||||
if (settings.DownloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
add_paused = settings.AddPaused,
|
||||
remove_at_ratio = false
|
||||
};
|
||||
options.download_location = settings.DownloadDirectory;
|
||||
}
|
||||
|
||||
if (settings.CompletedDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
options.move_completed_path = settings.CompletedDirectory;
|
||||
options.move_completed = true;
|
||||
}
|
||||
|
||||
var response = ProcessRequest<string>(settings, "core.add_torrent_magnet", magnetLink, options);
|
||||
|
||||
@@ -114,11 +126,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
public string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings)
|
||||
{
|
||||
var options = new
|
||||
dynamic options = new ExpandoObject();
|
||||
|
||||
options.add_paused = settings.AddPaused;
|
||||
options.remove_at_ratio = false;
|
||||
|
||||
if (settings.DownloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
add_paused = settings.AddPaused,
|
||||
remove_at_ratio = false
|
||||
};
|
||||
options.download_location = settings.DownloadDirectory;
|
||||
}
|
||||
|
||||
if (settings.CompletedDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
options.move_completed_path = settings.CompletedDirectory;
|
||||
options.move_completed = true;
|
||||
}
|
||||
|
||||
var response = ProcessRequest<string>(settings, "core.add_torrent_file", filename, fileContent, options);
|
||||
|
||||
|
||||
@@ -61,6 +61,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
[FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
|
||||
public bool AddPaused { get; set; }
|
||||
|
||||
[FieldDefinition(10, Label = "DownloadClientDelugeSettingsDirectory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryHelpText")]
|
||||
public string DownloadDirectory { get; set; }
|
||||
|
||||
[FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")]
|
||||
public string CompletedDirectory { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -19,7 +19,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IDownloadClient>))]
|
||||
[CheckOn(typeof(ModelEvent<RootFolder>))]
|
||||
[CheckOn(typeof(ModelEvent<RemotePathMapping>))]
|
||||
|
||||
public class DownloadClientRootFolderCheck : HealthCheckBase, IProvideHealthCheck
|
||||
{
|
||||
private readonly IProvideDownloadClient _downloadClientProvider;
|
||||
@@ -58,7 +57,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot", new Dictionary<string, object>
|
||||
{
|
||||
{ "downloadClientName", client.Definition.Name },
|
||||
{ "path", folder.FullPath }
|
||||
{ "rootFolderPath", folder.FullPath }
|
||||
}),
|
||||
"#downloads-in-root-folder");
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Movies.Collections;
|
||||
using NzbDrone.Core.Movies.Events;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(CollectionRefreshCompleteEvent))]
|
||||
[CheckOn(typeof(ModelEvent<RootFolder>))]
|
||||
public class MovieCollectionRootFolderCheck : HealthCheckBase
|
||||
{
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
"ChmodFolder": "Složka chmod",
|
||||
"CertificationCountryHelpText": "Vyberte zemi pro certifikaci filmu",
|
||||
"Cancel": "Zrušit",
|
||||
"ChownGroupHelpTextWarning": "Funguje to pouze v případě, že je uživatel souboru {appName} vlastníkem souboru. Je lepší zajistit, aby stahovací klient používal stejnou skupinu jako {appName}.",
|
||||
"ChownGroupHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient stahování používal stejnou skupinu jako {appName}.",
|
||||
"ClientPriority": "Priorita klienta",
|
||||
"Component": "Komponenta",
|
||||
"ClickToChangeQuality": "Kliknutím změníte kvalitu",
|
||||
@@ -212,13 +212,12 @@
|
||||
"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}",
|
||||
"ChmodFolderHelpText": "Octal, aplikováno během importu / přejmenování na mediální složky a soubory (bez provádění bitů)",
|
||||
"AgeWhenGrabbed": "Stáří (kdy bylo získáno)",
|
||||
"ChmodFolderHelpTextWarning": "Funguje to pouze v případě, že je uživatel souboru {appName} vlastníkem souboru. Je lepší zajistit, aby stahovací klient správně nastavil oprávnění.",
|
||||
"ChmodFolderHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.",
|
||||
"Updates": "Aktualizace",
|
||||
"Uptime": "Provozuschopnost",
|
||||
"ChownGroupHelpText": "Název skupiny nebo gid. Použijte gid pro vzdálené systémy souborů.",
|
||||
@@ -1098,7 +1097,7 @@
|
||||
"DeletedReasonMissingFromDisk": "{appName}u se nepodařilo najít soubor na disku, proto byl soubor odpojen od filmu v databázi",
|
||||
"DeletedReasonUpgrade": "Soubor byl odstraněn pro import lepší verze",
|
||||
"DeleteSelectedMovieFilesHelpText": "Opravdu chcete smazat vybrané soubory filmů?",
|
||||
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {1}.",
|
||||
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {appName}.",
|
||||
"DownloadClientsLoadError": "Nelze načíst klienty pro stahování",
|
||||
"EditSelectedMovies": "Upravit vybrané filmy",
|
||||
"EditSelectedDownloadClients": "Upravit vybrané klienty pro stahování",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"CompletedDownloadHandling": "Download-Handhabung abgeschlossen",
|
||||
"Connect": "Verbinden",
|
||||
"Connections": "Verbindungen",
|
||||
"Crew": "Besatzung",
|
||||
"Crew": "Besetzung",
|
||||
"CustomFilters": "Benutzerdefinierte Filter",
|
||||
"CustomFormats": "Eigene Formate",
|
||||
"Date": "Datum",
|
||||
@@ -149,7 +149,7 @@
|
||||
"Language": "Sprache",
|
||||
"MediaManagementSettingsSummary": "Einstellungen für Bennenung und Dateiverwaltung",
|
||||
"Year": "Jahr",
|
||||
"Wanted": "› Gesucht",
|
||||
"Wanted": "Gesucht",
|
||||
"UpdateSelected": "Auswahl aktualisieren",
|
||||
"UiSettingsSummary": "Einstellungen für Kalender, Datumsformat und Farbbeeinträchtigung",
|
||||
"Timeleft": "Restzeit",
|
||||
@@ -386,7 +386,6 @@
|
||||
"ImportMovies": "Filme importieren",
|
||||
"IncludeCustomFormatWhenRenaming": "Eigenes Format beim umbennen einfügen",
|
||||
"IncludeCustomFormatWhenRenamingHelpText": "In {Custom Formats} umbennenungs Format",
|
||||
"IncludeHealthWarningsHelpText": "Zustandswarnung",
|
||||
"IncludeUnmonitored": "Nicht beobachtete einbeziehen",
|
||||
"IndexerFlags": "Indexer-Flags",
|
||||
"Interval": "Intervall",
|
||||
@@ -549,7 +548,7 @@
|
||||
"PreferIndexerFlags": "Bevorzugte Indexer Flags",
|
||||
"CopyUsingHardlinksMovieHelpText": "Hardlinks erlauben es {appName}, Torrents zu importieren die derzeit geseeded werden, ohne dabei weiteren Speicherplatz zu belegen oder die Datei vollständig zu kopieren. Hardlinks funktionieren nur, wenn sich die Quelle und das Ziel auf dem selben Volume befinden",
|
||||
"SearchForMovie": "Film suchen",
|
||||
"AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich.",
|
||||
"AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich",
|
||||
"CopyUsingHardlinksHelpTextWarning": "Gelegentlich können Dateisperren das Umbenennen von Dateien verhindern, die geseedet werden. Sie können das Seeding vorübergehend deaktivieren und als Workaround die Umbenennungsfunktion von {appName} verwenden.",
|
||||
"IndexerSettings": "Indexer Einstellungen",
|
||||
"ProxyPasswordHelpText": "Sie müssen nur einen Benutzernamen und ein Passwort eingeben, wenn dies erforderlich ist. Andernfalls lassen Sie sie leer.",
|
||||
@@ -624,16 +623,16 @@
|
||||
"DownloadClientUnavailable": "Downloader ist nicht verfügbar",
|
||||
"DeleteTagMessageText": "Sind Sie sicher, dass Sie das Tag „{label}“ löschen möchten?",
|
||||
"DeleteRestrictionHelpText": "Beschränkung '{0}' wirklich löschen?",
|
||||
"DeleteNotificationMessageText": "Sind Sie sicher, dass Sie die Benachrichtigung „{name}“ löschen möchten?",
|
||||
"DeleteIndexerMessageText": "Sind Sie sicher, dass Sie den Indexer „{name}“ löschen möchten?",
|
||||
"DeleteDownloadClientMessageText": "Sind Sie sicher, dass Sie den Download-Client „{name}“ löschen möchten?",
|
||||
"DeleteBackupMessageText": "Sind Sie sicher, dass Sie die Sicherung „{name}“ löschen möchten?",
|
||||
"DeleteNotificationMessageText": "Bist du sicher, dass du die Benachrichtigung '{name}' wirklich löschen willst?",
|
||||
"DeleteIndexerMessageText": "Bist du sicher, dass du den Indexer '{name}' wirklich löschen willst?",
|
||||
"DeleteDownloadClientMessageText": "Bist du sicher, dass du den Download Client '{name}' wirklich löschen willst?",
|
||||
"DeleteBackupMessageText": "Soll das Backup '{name}' wirklich gelöscht werden?",
|
||||
"Cutoff": "Schwelle",
|
||||
"ClickToChangeMovie": "Klicken um den Film zu bearbeiten",
|
||||
"CheckDownloadClientForDetails": "Weitere Informationen finden Sie im Download-Client",
|
||||
"CancelPendingTask": "Möchten Sie diese ausstehende Aufgabe wirklich abbrechen?",
|
||||
"BranchUpdateMechanism": "Git-Branch für den externen Updateablauf",
|
||||
"BranchUpdate": "Verwendeter Git-Branch für Aktualisierungen",
|
||||
"BranchUpdate": "Branch, der verwendet werden soll, um {appName} zu updaten",
|
||||
"BeforeUpdate": "Vor dem Update",
|
||||
"AddingTag": "Tag hinzufügen",
|
||||
"YouCanAlsoSearch": "Es kann auch nach der TMDb oder IMDb ID eines Filmes gesucht werden. Z.B.: 'tmdb:71663'",
|
||||
@@ -707,7 +706,6 @@
|
||||
"ImportCustomFormat": "Eigenes Format importieren",
|
||||
"ExportCustomFormat": "Eigenes Format exportieren",
|
||||
"CustomFormatUnknownConditionOption": "Unbekannte Option '{0}' für die Bedingung '{1}'",
|
||||
"CustomFormatUnknownCondition": "Unbekannte eigene Format Bedingung '{0}'",
|
||||
"DownloadPropersAndRepacksHelpTextCustomFormat": "Benutze 'Nicht bevorzugen' um den eigene Formate Score über Proper oder Repacks zu sortieren",
|
||||
"DownloadPropersAndRepacksHelpTextWarning": "Benutze eigene Formate um automatisch auf Proper oder Repack releases zu upgraden",
|
||||
"DownloadPropersAndRepacksHelpText": "Automatisch Proper oder Repacks zum upgraden eines Filmes zulassen",
|
||||
@@ -927,10 +925,10 @@
|
||||
"AllMoviesInPathHaveBeenImported": "Alle Filme in {path} wurden importiert",
|
||||
"AllFiles": "Alle Dateien",
|
||||
"AfterManualRefresh": "Nach manueller Aktualisierung",
|
||||
"AddToDownloadQueue": "Zur Download-Warteschlange hinzufügen",
|
||||
"AddToDownloadQueue": "Zur Download Warteschlange hinzufügen",
|
||||
"AddRootFolder": "Stammverzeichnis hinzufügen",
|
||||
"AddQualityProfile": "Qualitäts Profil hinzufügen",
|
||||
"AddedToDownloadQueue": "Zur Download-Warteschlange hinzugefügt",
|
||||
"AddedToDownloadQueue": "Zur Download Warteschlange hinzugefügt",
|
||||
"AddDownloadClient": "Downloadmanager hinzufügen",
|
||||
"AddCustomFormat": "Eigenes Format hinzufügen",
|
||||
"AddDelayProfile": "Verzögerungsprofil hinzufügen",
|
||||
@@ -1081,7 +1079,7 @@
|
||||
"RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?",
|
||||
"ResetDefinitionTitlesHelpText": "Definitionstitel und -werte zurücksetzen",
|
||||
"DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{0}' löschen willst?",
|
||||
"DeleteCustomFormatMessageText": "Bist du sicher, dass du das eigene Format '{name}' löschen willst?",
|
||||
"DeleteCustomFormatMessageText": "Bist du sicher, dass du das benutzerdefinierte Format '{name}' wirklich löschen willst?",
|
||||
"DeleteDelayProfileMessageText": "Sind Sie sicher, dass Sie dieses Verzögerungsprofil löschen möchten?",
|
||||
"DeleteFormatMessageText": "Bist du sicher, dass du das Formatierungstag {0} löschen willst?",
|
||||
"ResetAPIKeyMessageText": "Sind Sie sicher, dass Sie Ihren API-Schlüssel zurücksetzen möchten?",
|
||||
@@ -1111,24 +1109,24 @@
|
||||
"NoImportListsFound": "Keine Einspiel-Listen gefunden",
|
||||
"NoIndexersFound": "Keine Indexer gefunden",
|
||||
"CountIndexersSelected": "{count} Indexer ausgewählt",
|
||||
"ApplyTagsHelpTextAdd": "Hinzufügen: Fügen Sie die Tags der vorhandenen Tag-Liste hinzu",
|
||||
"ApplyTagsHelpTextRemove": "Entfernen: Die eingegebenen Tags entfernen",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "So wenden Sie Tags auf die ausgewählten Indexer an",
|
||||
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetzen Sie die Tags durch die eingegebenen Tags (geben Sie keine Tags ein, um alle Tags zu löschen).",
|
||||
"ApplyTagsHelpTextAdd": "Hinzufügen: Füge Tags zu den bestehenden Tags hinzu",
|
||||
"ApplyTagsHelpTextRemove": "Entfernen: Entferne die hinterlegten Tags",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Wie Tags zu den selektierten Indexern hinzugefügt werden können",
|
||||
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetze die Tags mit den eingegebenen Tags (keine Tags eingeben um alle Tags zu löschen)",
|
||||
"DeleteImportList": "Importliste löschen",
|
||||
"DeleteImportListMessageText": "Sind Sie sicher, dass Sie die Liste „{name}“ löschen möchten?",
|
||||
"DeleteQualityProfileMessageText": "Sind Sie sicher, dass Sie das Qualitätsprofil „{name}“ löschen möchten?",
|
||||
"DeleteImportListMessageText": "Bist du sicher, dass du die Liste '{name}' wirklich löschen willst?",
|
||||
"DeleteQualityProfileMessageText": "Bist du sicher, dass du das Qualitätsprofil '{name}' wirklich löschen willst?",
|
||||
"RemoveQueueItemConfirmation": "Bist du sicher, dass du {0} Einträge aus der Warteschlange entfernen willst?",
|
||||
"SkipRedownload": "Überspringe erneuten Download",
|
||||
"MoveAutomatically": "Automatisch verschieben",
|
||||
"AutoTaggingNegateHelpText": "Wenn aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.",
|
||||
"AutoTaggingRequiredHelpText": "Diese {0} Bedingungen müsen zutreffen damit das eigene Format zutrifft. Ansonsten reicht ein einzelner {1} Treffer.",
|
||||
"AutoTaggingNegateHelpText": "Falls aktiviert wird die Auto Tagging Regel nicht angewendet, solange diese Bedingung {implementationName} zutrifft.",
|
||||
"AutoTaggingRequiredHelpText": "Diese {0} Bedingungen müssen erfüllt sein, damit das eigene Format zutrifft. Ansonsten reicht ein einzelner {1} Treffer.",
|
||||
"DeleteAutoTagHelpText": "Sind Sie sicher, dass Sie das automatische Tag „{name}“ löschen möchten?",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte Download-Clients löschen möchten?",
|
||||
"DeleteSelectedImportListsMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte Importliste(n) löschen möchten?",
|
||||
"DeleteSelectedIndexersMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte(n) Indexer löschen möchten?",
|
||||
"ApplyTagsHelpTextHowToApplyDownloadClients": "So wenden Sie Tags auf die ausgewählten Download-Clients an",
|
||||
"ApplyTagsHelpTextHowToApplyImportLists": "So wenden Sie Tags auf die ausgewählten Importlisten an",
|
||||
"ApplyTagsHelpTextHowToApplyDownloadClients": "Wie Tags zu den selektierten Downloadclients hinzugefügt werden können",
|
||||
"ApplyTagsHelpTextHowToApplyImportLists": "Wie Tags den selektierten Importlisten hinzugefügt werden können",
|
||||
"DeleteRootFolderMessageText": "Sind Sie sicher, dass Sie den Stammordner „{path}“ löschen möchten?",
|
||||
"DeleteRootFolder": "Stammordner löschen",
|
||||
"AddConnection": "Verbindung hinzufügen",
|
||||
@@ -1170,13 +1168,13 @@
|
||||
"ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.",
|
||||
"CountImportListsSelected": "{count} Importliste(n) ausgewählt",
|
||||
"CustomFormatJson": "Benutzerdefiniertes JSON-Format",
|
||||
"DefaultNameCopiedProfile": "{Name} – Kopieren",
|
||||
"DelayingDownloadUntil": "Download wird bis zum {Datum} um {Uhrzeit} verzögert",
|
||||
"DefaultNameCopiedProfile": "{name} – Kopieren",
|
||||
"DelayingDownloadUntil": "Download wird bis zum {date} um {time} verzögert",
|
||||
"DeleteAutoTag": "Auto-Tag löschen",
|
||||
"DeletedReasonManual": "Die Datei wurde über die Benutzeroberfläche gelöscht",
|
||||
"DeletedReasonUpgrade": "Die Datei wurde gelöscht, um ein Upgrade zu importieren",
|
||||
"RemoveTagsAutomatically": "Tags automatisch entfernen",
|
||||
"RetryingDownloadOn": "Erneuter Downloadversuch am {Datum} um {Uhrzeit}",
|
||||
"RetryingDownloadOn": "Erneuter Downloadversuch am {date} um {time}",
|
||||
"SetReleaseGroupModalTitle": "{modalTitle} – Release-Gruppe festlegen",
|
||||
"UpdaterLogFiles": "Updater-Protokolldateien",
|
||||
"Default": "Standard",
|
||||
@@ -1195,11 +1193,32 @@
|
||||
"True": "WAHR",
|
||||
"AuthenticationMethod": "Authentifizierungsmethode",
|
||||
"AutoRedownloadFailedFromInteractiveSearchHelpText": "Suchen Sie automatisch nach einer anderen Version und versuchen Sie, sie herunterzuladen, wenn eine fehlerhafte Version aus der interaktiven Suche ausgewählt wurde",
|
||||
"DefaultNameCopiedSpecification": "{Name} – Kopieren",
|
||||
"DefaultNameCopiedSpecification": "{name} – Kopieren",
|
||||
"CountDownloadClientsSelected": "{count} Download-Client(s) ausgewählt",
|
||||
"AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden",
|
||||
"ClearBlocklist": "Sperrliste leeren",
|
||||
"CloneAutoTag": "Automatische Tags kopieren",
|
||||
"Retention": "Aufbewahrung ( Retention )",
|
||||
"Unmonitored": "Nicht beobachtet"
|
||||
"Unmonitored": "Nicht beobachtet",
|
||||
"DownloadClientFreeboxUnableToReachFreebox": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellungen „Host“, „Port“ oder „SSL verwenden“. (Fehler: {exceptionMessage})",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sie müssen sich bei Ihrer Diskstation als {username} anmelden und sie manuell in den DownloadStation-Einstellungen unter BT/HTTP/FTP/NZB -> Standort einrichten.",
|
||||
"UsenetBlackhole": "Usenet Blackhole",
|
||||
"BlocklistAndSearch": "Sperrliste und Suche",
|
||||
"BlocklistAndSearchHint": "Starte Suche nach einer Alternative, falls es der Sperrliste hinzugefügt wurde",
|
||||
"BlocklistMultipleOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternativen zu suchen",
|
||||
"ChangeCategory": "Kategorie wechseln",
|
||||
"BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert",
|
||||
"BlackholeWatchFolderHelpText": "Der Ordner, aus dem {appName} fertige Downloads importieren soll",
|
||||
"BlocklistAndSearchMultipleHint": "Starte Suchen nach einer Alternative, falls es der Sperrliste hinzugefügt wurde",
|
||||
"BlocklistOnly": "Nur der Sperrliste hinzufügen",
|
||||
"Category": "Kategorie",
|
||||
"ClickToChangeIndexerFlags": "Klicken, um Indexer-Flags zu ändern",
|
||||
"AddReleaseProfile": "Release Profil hinzufügen",
|
||||
"BlocklistReleaseHelpText": "Dieses Release für erneuten Download durch {appName} via RSS oder automatische Suche sperren",
|
||||
"AddListExclusion": "Listenausschluss hinzufügen",
|
||||
"ChownGroup": "chown Gruppe",
|
||||
"Clone": "Klonen",
|
||||
"BlocklistOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternative zu suchen",
|
||||
"AddAutoTagError": "Auto-Tag konnte nicht hinzugefügt werden. Bitte erneut versuchen.",
|
||||
"AddDelayProfileError": "Verzögerungsprofil konnte nicht hinzugefügt werden. Bitte erneut versuchen."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user