mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
86 Commits
v4.0.0.836
...
v4.0.1.104
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200396ef7a | ||
|
|
c5a724f14e | ||
|
|
42b11528b4 | ||
|
|
e2210228b3 | ||
|
|
ded7c3c6e2 | ||
|
|
e1c6722aad | ||
|
|
e17655c26a | ||
|
|
e66c628241 | ||
|
|
8f0514a91d | ||
|
|
d7aea82e45 | ||
|
|
19db75b36b | ||
|
|
11a18b534a | ||
|
|
70807a9dcf | ||
|
|
350600607d | ||
|
|
e9f0c96249 | ||
|
|
d9acbf5682 | ||
|
|
07cbd7c8d2 | ||
|
|
0ea189d03c | ||
|
|
9e3f9f9618 | ||
|
|
68c326ae27 | ||
|
|
46367d2023 | ||
|
|
b64c52a846 | ||
|
|
345854d0fe | ||
|
|
31baed4b2c | ||
|
|
7d0d503a5e | ||
|
|
9f50166fa6 | ||
|
|
3c1ca6ea4e | ||
|
|
3cd4c67ba1 | ||
|
|
fc3a2e9ab2 | ||
|
|
a71d40edba | ||
|
|
9ba5850fca | ||
|
|
0d06418194 | ||
|
|
f95dd00b51 | ||
|
|
271266b10a | ||
|
|
cab93249ec | ||
|
|
8921c5d7a0 | ||
|
|
dbbf1a7f58 | ||
|
|
69f99373e5 | ||
|
|
7be5732a3a | ||
|
|
e66ba84fc0 | ||
|
|
c0b30a5028 | ||
|
|
3cf4d2907e | ||
|
|
ae96ebca57 | ||
|
|
d336aaf3f0 | ||
|
|
ec40bc6eea | ||
|
|
75bb34afaa | ||
|
|
de1cc25c90 | ||
|
|
9884f6f282 | ||
|
|
e792db4d33 | ||
|
|
2dbf5b5a71 | ||
|
|
c6bb6ad878 | ||
|
|
bfd24da2d9 | ||
|
|
8dd3b45c90 | ||
|
|
0b0f21d0ac | ||
|
|
738f5c58ad | ||
|
|
9a7b5bf14e | ||
|
|
e1260d504e | ||
|
|
736651324f | ||
|
|
489f03441b | ||
|
|
e4b5d559df | ||
|
|
07fbb0d1f4 | ||
|
|
666455f9b1 | ||
|
|
091449d9bf | ||
|
|
f87a66fcba | ||
|
|
1bba7e177b | ||
|
|
57445bbe57 | ||
|
|
ec91142c85 | ||
|
|
0685896ed8 | ||
|
|
ee0048c768 | ||
|
|
16e5ffa467 | ||
|
|
431c66c7c1 | ||
|
|
57bd6539c8 | ||
|
|
637cb1711d | ||
|
|
7e011df2b2 | ||
|
|
79907c881c | ||
|
|
db6a627983 | ||
|
|
d76a489be6 | ||
|
|
fd17df0dd0 | ||
|
|
53cf530893 | ||
|
|
91f33c670e | ||
|
|
d322619733 | ||
|
|
1182798929 | ||
|
|
ad0249c7db | ||
|
|
3259e6dc10 | ||
|
|
541d3307e1 | ||
|
|
ad60352bae |
@@ -203,6 +203,7 @@ dotnet_diagnostic.CA1819.severity = suggestion
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
||||
dotnet_diagnostic.CA1823.severity = suggestion
|
||||
dotnet_diagnostic.CA1824.severity = suggestion
|
||||
dotnet_diagnostic.CA1825.severity = suggestion
|
||||
dotnet_diagnostic.CA2000.severity = suggestion
|
||||
dotnet_diagnostic.CA2002.severity = suggestion
|
||||
dotnet_diagnostic.CA2007.severity = suggestion
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Bug Report
|
||||
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Only bug reports for v4 will be accepted, older versions are EOL & unsupported.'
|
||||
description: 'Only bug reports for v4 will be accepted, older versions are no longer receiving bug fixes and support issues will be closed immediately.'
|
||||
labels: ['needs-triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,14 +1,15 @@
|
||||
#### Database Migration
|
||||
YES | NO
|
||||
|
||||
#### Description
|
||||
A few sentences describing the overall goals of the pull request's commits.
|
||||
|
||||
#### Todos
|
||||
- [ ] Tests
|
||||
- [ ] Wiki Updates
|
||||
<!-- Remove any of the following sections if they are not used -->
|
||||
|
||||
#### Screenshots for UI Changes
|
||||
|
||||
|
||||
#### Database Migration
|
||||
YES - ###
|
||||
|
||||
|
||||
#### Issues Fixed or Closed by this PR
|
||||
* Closes #
|
||||
|
||||
*
|
||||
|
||||
13
.github/actions/test/action.yml
vendored
13
.github/actions/test/action.yml
vendored
@@ -77,11 +77,20 @@ runs:
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger trx --results-directory "${{ env.RESULTS_NAME }}"
|
||||
run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx"
|
||||
|
||||
- name: Upload Test Results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: results-${{ env.RESULTS_NAME }}
|
||||
path: ${{ env.RESULTS_NAME }}/*.trx
|
||||
path: TestResults/*.trx
|
||||
|
||||
- name: Publish Test Results
|
||||
uses: phoenix-actions/test-reporting@v12
|
||||
with:
|
||||
name: Test Results
|
||||
output-to: step-summary
|
||||
path: '*.trx'
|
||||
reporter: dotnet-trx
|
||||
working-directory: TestResults
|
||||
|
||||
6
.github/workflows/api_docs.yml
vendored
6
.github/workflows/api_docs.yml
vendored
@@ -31,12 +31,6 @@ jobs:
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v3
|
||||
id: setup-dotnet
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
- name: Create temporary global.json
|
||||
run: |
|
||||
echo '{"sdk":{"version": "${{ steps.setup-dotnet.outputs.dotnet-version }}" } }' > ./global.json
|
||||
|
||||
- name: Create openapi.json
|
||||
run: ./docs.sh Linux
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
FRAMEWORK: net6.0
|
||||
BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
SONARR_MAJOR_VERSION: 4
|
||||
VERSION: 4.0.0
|
||||
VERSION: 4.0.1
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
binary_path: osx-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||
- os: windows-latest
|
||||
artifact: tests-win-x64
|
||||
filter: TestCategory!=ManualTest&TestCategory=WINDOWS&TestCategory=IntegrationTest
|
||||
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest
|
||||
binary_artifact: build_windows
|
||||
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
21
.github/workflows/publish-test-results.yml
vendored
21
.github/workflows/publish-test-results.yml
vendored
@@ -13,12 +13,29 @@ permissions:
|
||||
|
||||
jobs:
|
||||
report:
|
||||
if: ${{ github.event.workflow_run.conclusion != 'skipped' && github.event.workflow_run.conclusion != 'cancelled' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Test Reports
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: test-results
|
||||
pattern: results-*
|
||||
merge-multiple: true
|
||||
repository: ${{ github.event.repository.owner.login }}/${{ github.event.repository.name }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish Test Results
|
||||
uses: phoenix-actions/test-reporting@v12
|
||||
with:
|
||||
artifact: /results-(.*)/
|
||||
name: '$1'
|
||||
list-suites: failed
|
||||
list-tests: failed
|
||||
name: Test Results
|
||||
only-summary: true
|
||||
path: '*.trx'
|
||||
reporter: dotnet-trx
|
||||
working-directory: test-results
|
||||
|
||||
@@ -98,7 +98,7 @@ echo "Directories created"
|
||||
echo ""
|
||||
echo "Installing pre-requisite Packages"
|
||||
# shellcheck disable=SC2086
|
||||
apt update && apt install $app_prereq
|
||||
apt update && apt install -y $app_prereq
|
||||
echo ""
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
# get arch
|
||||
|
||||
@@ -2,6 +2,8 @@ const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryEventTypeCell.css';
|
||||
|
||||
function getIconName(eventType) {
|
||||
function getIconName(eventType, data) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return icons.DOWNLOADING;
|
||||
@@ -17,7 +17,7 @@ function getIconName(eventType) {
|
||||
case 'downloadFailed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'episodeFileDeleted':
|
||||
return icons.DELETE;
|
||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||
case 'episodeFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
|
||||
case 'downloadFailed':
|
||||
return translate('DownloadFailedEpisodeTooltip');
|
||||
case 'episodeFileDeleted':
|
||||
return translate('EpisodeFileDeletedTooltip');
|
||||
return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip');
|
||||
case 'episodeFileRenamed':
|
||||
return translate('EpisodeFileRenamedTooltip');
|
||||
case 'downloadIgnored':
|
||||
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, data }) {
|
||||
const iconName = getIconName(eventType);
|
||||
const iconName = getIconName(eventType, data);
|
||||
const iconKind = getIconKind(eventType);
|
||||
const tooltip = getTooltip(eventType, data);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -305,9 +305,16 @@ class Queue extends Component {
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
@@ -315,7 +322,7 @@ class Queue extends Component {
|
||||
return !!(item && item.seriesId && item.episodeId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
|
||||
@@ -99,7 +99,9 @@ class QueueRow extends Component {
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
@@ -362,6 +364,15 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -410,6 +421,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!series}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -440,7 +452,9 @@ QueueRow.propTypes = {
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
sizeleft: PropTypes.number,
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('RemoveQueueItem', { sourceTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
remove: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
skipRedownload: boolean;
|
||||
}
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
selectedCount?: number;
|
||||
onRemovePress(props: RemovePressProps): void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
selectedCount,
|
||||
onRemovePress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedCount === 1) {
|
||||
return {
|
||||
title: translate('RemoveSelectedItem'),
|
||||
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: translate('RemoveSelectedItems'),
|
||||
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||
selectedCount,
|
||||
}),
|
||||
};
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
hint: multipleSelected
|
||||
? translate('RemoveMultipleFromDownloadClientHint')
|
||||
: translate('RemoveFromDownloadClientHint'),
|
||||
},
|
||||
{
|
||||
key: 'changeCategory',
|
||||
value: translate('ChangeCategory'),
|
||||
isDisabled: !canChangeCategory,
|
||||
hint: multipleSelected
|
||||
? translate('ChangeCategoryMultipleHint')
|
||||
: translate('ChangeCategoryHint'),
|
||||
},
|
||||
{
|
||||
key: 'ignore',
|
||||
value: multipleSelected
|
||||
? translate('IgnoreDownloads')
|
||||
: translate('IgnoreDownload'),
|
||||
isDisabled: !canIgnore,
|
||||
hint: multipleSelected
|
||||
? translate('IgnoreDownloadsHint')
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
hint: translate('DoNotBlocklistHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
remove: removalMethod === 'removeFromClient',
|
||||
changeCategory: removalMethod === 'changeCategory',
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>{message}</div>
|
||||
|
||||
{isPending ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="removalMethod"
|
||||
value={removalMethod}
|
||||
values={removalMethodOptions}
|
||||
isDisabled={!canChangeCategory && !canIgnore}
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{multipleSelected
|
||||
? translate('BlocklistReleases')
|
||||
: translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="blocklistMethod"
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
@@ -1,174 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
||||
@@ -44,7 +44,16 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
|
||||
@@ -7,6 +7,7 @@ import AppSectionState, {
|
||||
import Language from 'Language/Language';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
@@ -35,10 +36,15 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface ImportListOptionsSettingsAppState
|
||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
downloadClients: DownloadClientAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexers: IndexerAppState;
|
||||
@@ -46,6 +52,7 @@ interface SettingsAppState {
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
ui: UiSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
@@ -52,6 +52,10 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
|
||||
.statusContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:global(.fullColor) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
|
||||
@@ -102,7 +102,12 @@ class CalendarEvent extends Component {
|
||||
{series.title}
|
||||
</div>
|
||||
|
||||
<div className={styles.statusContainer}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.statusContainer,
|
||||
fullColorEvents && 'fullColor'
|
||||
)}
|
||||
>
|
||||
{
|
||||
missingAbsoluteNumber ?
|
||||
<Icon
|
||||
@@ -128,6 +133,7 @@ class CalendarEvent extends Component {
|
||||
<span className={styles.statusIcon}>
|
||||
<CalendarEventQueueDetails
|
||||
{...queueItem}
|
||||
fullColorEvents={fullColorEvents}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
@@ -150,7 +156,7 @@ class CalendarEvent extends Component {
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.EPISODE_FILE}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('QualityCutoffNotMet')}
|
||||
/> :
|
||||
null
|
||||
@@ -160,9 +166,8 @@ class CalendarEvent extends Component {
|
||||
episodeNumber === 1 && seasonNumber > 0 ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
name={icons.PREMIERE}
|
||||
kind={kinds.INFO}
|
||||
darken={fullColorEvents}
|
||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||
/> :
|
||||
null
|
||||
@@ -173,8 +178,8 @@ class CalendarEvent extends Component {
|
||||
finaleType ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
name={finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON}
|
||||
kind={finaleType === 'series' ? kinds.DANGER : kinds.WARNING}
|
||||
title={getFinaleTypeName(finaleType)}
|
||||
/> :
|
||||
null
|
||||
@@ -187,7 +192,6 @@ class CalendarEvent extends Component {
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={kinds.PINK}
|
||||
darken={fullColorEvents}
|
||||
title={translate('Special')}
|
||||
/> :
|
||||
null
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
.expandContainer,
|
||||
.collapseContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -50,6 +51,15 @@
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.statusContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:global(.fullColor) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface CssExports {
|
||||
'onAir': string;
|
||||
'premiere': string;
|
||||
'seriesTitle': string;
|
||||
'statusContainer': string;
|
||||
'statusIcon': string;
|
||||
'unaired': string;
|
||||
'unmonitored': string;
|
||||
|
||||
@@ -145,45 +145,51 @@ class CalendarEventGroup extends Component {
|
||||
{series.title}
|
||||
</div>
|
||||
|
||||
{
|
||||
isMissingAbsoluteNumber &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.WARNING}
|
||||
title={translate('EpisodeMissingAbsoluteNumber')}
|
||||
/>
|
||||
}
|
||||
<div
|
||||
className={classNames(
|
||||
styles.statusContainer,
|
||||
fullColorEvents && 'fullColor'
|
||||
)}
|
||||
>
|
||||
{
|
||||
isMissingAbsoluteNumber &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.WARNING}
|
||||
title={translate('EpisodeMissingAbsoluteNumber')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
anyDownloading &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.DOWNLOADING}
|
||||
title={translate('AnEpisodeIsDownloading')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
anyDownloading &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.DOWNLOADING}
|
||||
title={translate('AnEpisodeIsDownloading')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={kinds.INFO}
|
||||
darken={fullColorEvents}
|
||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.PREMIERE}
|
||||
kind={kinds.INFO}
|
||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
showFinaleIcon &&
|
||||
lastEpisode.finaleType ?
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
title={getFinaleTypeName(lastEpisode.finaleType)}
|
||||
/> : null
|
||||
}
|
||||
{
|
||||
showFinaleIcon &&
|
||||
lastEpisode.finaleType ?
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={lastEpisode.finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON}
|
||||
kind={lastEpisode.finaleType === 'series' ? kinds.DANGER : kinds.WARNING}
|
||||
title={getFinaleTypeName(lastEpisode.finaleType)}
|
||||
/> : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.airingInfo}>
|
||||
@@ -218,16 +224,19 @@ class CalendarEventGroup extends Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
showEpisodeInformation &&
|
||||
showEpisodeInformation ?
|
||||
<Link
|
||||
className={styles.expandContainer}
|
||||
component="div"
|
||||
onPress={this.onExpandPress}
|
||||
>
|
||||
|
||||
<Icon
|
||||
name={icons.EXPAND}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
</Link> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ function createIsDownloadingSelector() {
|
||||
(state) => state.queue.details,
|
||||
(episodeIds, details) => {
|
||||
return details.items.some((item) => {
|
||||
return item.episode && episodeIds.includes(item.episode.id);
|
||||
return !!(item.episodeId && episodeIds.includes(item.episodeId));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -22,9 +22,20 @@ function Legend(props) {
|
||||
if (showFinaleIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name="Finale"
|
||||
icon={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
name={translate('SeasonFinale')}
|
||||
icon={icons.FINALE_SEASON}
|
||||
kind={kinds.WARNING}
|
||||
fullColorEvents={fullColorEvents}
|
||||
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
||||
/>
|
||||
);
|
||||
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name={translate('SeriesFinale')}
|
||||
icon={icons.FINALE_SERIES}
|
||||
kind={kinds.DANGER}
|
||||
fullColorEvents={fullColorEvents}
|
||||
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
||||
/>
|
||||
);
|
||||
@@ -33,10 +44,10 @@ function Legend(props) {
|
||||
if (showSpecialIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name="Special"
|
||||
name={translate('Special')}
|
||||
icon={icons.INFO}
|
||||
kind={kinds.PINK}
|
||||
darken={fullColorEvents}
|
||||
fullColorEvents={fullColorEvents}
|
||||
tooltip={translate('SpecialEpisode')}
|
||||
/>
|
||||
);
|
||||
@@ -45,9 +56,10 @@ function Legend(props) {
|
||||
if (showCutoffUnmetIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name="Cutoff Not Met"
|
||||
name={translate('Cutoff Not Met')}
|
||||
icon={icons.EPISODE_FILE}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
fullColorEvents={fullColorEvents}
|
||||
tooltip={translate('QualityCutoffNotMet')}
|
||||
/>
|
||||
);
|
||||
@@ -112,10 +124,10 @@ function Legend(props) {
|
||||
|
||||
<div>
|
||||
<LegendIconItem
|
||||
name="Premiere"
|
||||
icon={icons.INFO}
|
||||
name={translate('Premiere')}
|
||||
icon={icons.PREMIERE}
|
||||
kind={kinds.INFO}
|
||||
darken={true}
|
||||
fullColorEvents={fullColorEvents}
|
||||
tooltip={translate('CalendarLegendSeriesPremiereTooltip')}
|
||||
/>
|
||||
|
||||
@@ -129,6 +141,12 @@ function Legend(props) {
|
||||
{iconsToShow[2]}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
iconsToShow.length > 3 &&
|
||||
<div>
|
||||
{iconsToShow[3]}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,4 +7,8 @@
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
|
||||
&:global(.fullColorEvents) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -6,9 +7,9 @@ import styles from './LegendIconItem.css';
|
||||
function LegendIconItem(props) {
|
||||
const {
|
||||
name,
|
||||
fullColorEvents,
|
||||
icon,
|
||||
kind,
|
||||
darken,
|
||||
tooltip
|
||||
} = props;
|
||||
|
||||
@@ -18,9 +19,11 @@ function LegendIconItem(props) {
|
||||
title={tooltip}
|
||||
>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
className={classNames(
|
||||
styles.icon,
|
||||
fullColorEvents && 'fullColorEvents'
|
||||
)}
|
||||
name={icon}
|
||||
darken={darken}
|
||||
kind={kind}
|
||||
/>
|
||||
|
||||
@@ -31,14 +34,10 @@ function LegendIconItem(props) {
|
||||
|
||||
LegendIconItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
fullColorEvents: PropTypes.bool.isRequired,
|
||||
icon: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
darken: PropTypes.bool.isRequired,
|
||||
tooltip: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
LegendIconItem.defaultProps = {
|
||||
darken: false
|
||||
};
|
||||
|
||||
export default LegendIconItem;
|
||||
|
||||
@@ -30,22 +30,24 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
|
||||
@@ -264,6 +264,7 @@ FormInputGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
isDisabled: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
min: PropTypes.number,
|
||||
|
||||
@@ -12,18 +12,10 @@
|
||||
|
||||
.info {
|
||||
color: var(--infoColor);
|
||||
|
||||
&:global(.darken) {
|
||||
color: color(var(--infoColor) shade(30%));
|
||||
}
|
||||
}
|
||||
|
||||
.pink {
|
||||
color: var(--pink);
|
||||
|
||||
&:global(.darken) {
|
||||
color: color(var(--pink) shade(30%));
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
|
||||
@@ -18,7 +18,6 @@ class Icon extends PureComponent {
|
||||
kind,
|
||||
size,
|
||||
title,
|
||||
darken,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -27,8 +26,7 @@ class Icon extends PureComponent {
|
||||
<FontAwesomeIcon
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
darken && 'darken'
|
||||
styles[kind]
|
||||
)}
|
||||
icon={name}
|
||||
spin={isSpinning}
|
||||
@@ -61,7 +59,6 @@ Icon.propTypes = {
|
||||
kind: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
darken: PropTypes.bool.isRequired,
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
fixedWidth: PropTypes.bool.isRequired
|
||||
};
|
||||
@@ -69,7 +66,6 @@ Icon.propTypes = {
|
||||
Icon.defaultProps = {
|
||||
kind: kinds.DEFAULT,
|
||||
size: 14,
|
||||
darken: false,
|
||||
isSpinning: false,
|
||||
fixedWidth: false
|
||||
};
|
||||
|
||||
@@ -40,18 +40,26 @@ class FilterMenuContent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
customFilters.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
customFilters.length > 0 ?
|
||||
<MenuItemSeparator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -41,6 +41,9 @@ import {
|
||||
faChevronCircleUp as fasChevronCircleUp,
|
||||
faCircle as fasCircle,
|
||||
faCircleDown as fasCircleDown,
|
||||
faCirclePause as fasCirclePause,
|
||||
faCirclePlay as fasCirclePlay,
|
||||
faCircleStop as fasCircleStop,
|
||||
faCloud as fasCloud,
|
||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||
faCog as fasCog,
|
||||
@@ -55,6 +58,7 @@ import {
|
||||
faEye as fasEye,
|
||||
faFastBackward as fasFastBackward,
|
||||
faFastForward as fasFastForward,
|
||||
faFileCircleQuestion as fasFileCircleQuestion,
|
||||
faFileExport as fasFileExport,
|
||||
faFileInvoice as farFileInvoice,
|
||||
faFilter as fasFilter,
|
||||
@@ -146,7 +150,10 @@ export const EXPORT = fasFileExport;
|
||||
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
||||
export const FATAL = fasTimesCircle;
|
||||
export const FILE = farFile;
|
||||
export const FILE_MISSING = fasFileCircleQuestion;
|
||||
export const FILTER = fasFilter;
|
||||
export const FINALE_SEASON = fasCirclePause;
|
||||
export const FINALE_SERIES = fasCircleStop;
|
||||
export const FOOTNOTE = fasAsterisk;
|
||||
export const FOLDER = farFolder;
|
||||
export const FOLDER_OPEN = fasFolderOpen;
|
||||
@@ -178,6 +185,7 @@ export const PARENT = fasLevelUpAlt;
|
||||
export const PARSE = fasCalculator;
|
||||
export const PAUSED = fasPause;
|
||||
export const PENDING = farClock;
|
||||
export const PREMIERE = fasCirclePlay;
|
||||
export const PROFILE = fasUser;
|
||||
export const POSTER = fasTh;
|
||||
export const QUEUED = fasCloud;
|
||||
|
||||
@@ -269,33 +269,6 @@ function InteractiveImportModalContent(
|
||||
const [interactiveImportErrorMessage, setInteractiveImportErrorMessage] =
|
||||
useState<string | null>(null);
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const [bulkSelectOptions, setBulkSelectOptions] = useState([
|
||||
{
|
||||
key: 'select',
|
||||
value: translate('SelectDropdown'),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'season',
|
||||
value: translate('SelectSeason'),
|
||||
},
|
||||
{
|
||||
key: 'episode',
|
||||
value: translate('SelectEpisodes'),
|
||||
},
|
||||
{
|
||||
key: 'quality',
|
||||
value: translate('SelectQuality'),
|
||||
},
|
||||
{
|
||||
key: 'releaseGroup',
|
||||
value: translate('SelectReleaseGroup'),
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
value: translate('SelectLanguage'),
|
||||
},
|
||||
]);
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
const previousIsDeleting = usePrevious(isDeleting);
|
||||
const dispatch = useDispatch();
|
||||
@@ -318,19 +291,72 @@ function InteractiveImportModalContent(
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const bulkSelectOptions = useMemo(() => {
|
||||
const { seasonSelectDisabled, episodeSelectDisabled } = items.reduce(
|
||||
(acc, item) => {
|
||||
if (!selectedIds.includes(item.id)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const lastSelectedSeason = acc.lastSelectedSeason;
|
||||
|
||||
acc.seasonSelectDisabled ||= !item.series;
|
||||
acc.episodeSelectDisabled ||=
|
||||
item.seasonNumber === undefined ||
|
||||
(lastSelectedSeason >= 0 && item.seasonNumber !== lastSelectedSeason);
|
||||
acc.lastSelectedSeason = item.seasonNumber ?? -1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
seasonSelectDisabled: false,
|
||||
episodeSelectDisabled: false,
|
||||
lastSelectedSeason: -1,
|
||||
}
|
||||
);
|
||||
|
||||
const options = [
|
||||
{
|
||||
key: 'select',
|
||||
value: translate('SelectDropdown'),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'season',
|
||||
value: translate('SelectSeason'),
|
||||
disabled: seasonSelectDisabled,
|
||||
},
|
||||
{
|
||||
key: 'episode',
|
||||
value: translate('SelectEpisodes'),
|
||||
disabled: episodeSelectDisabled,
|
||||
},
|
||||
{
|
||||
key: 'quality',
|
||||
value: translate('SelectQuality'),
|
||||
},
|
||||
{
|
||||
key: 'releaseGroup',
|
||||
value: translate('SelectReleaseGroup'),
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
value: translate('SelectLanguage'),
|
||||
},
|
||||
];
|
||||
|
||||
if (allowSeriesChange) {
|
||||
options.splice(1, 0, {
|
||||
key: 'series',
|
||||
value: translate('SelectSeries'),
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [allowSeriesChange, items, selectedIds]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (allowSeriesChange) {
|
||||
const newBulkSelectOptions = [...bulkSelectOptions];
|
||||
|
||||
newBulkSelectOptions.splice(1, 0, {
|
||||
key: 'series',
|
||||
value: translate('SelectSeries'),
|
||||
});
|
||||
|
||||
setBulkSelectOptions(newBulkSelectOptions);
|
||||
}
|
||||
|
||||
if (initialSortKey) {
|
||||
const sortProps: { sortKey: string; sortDirection?: string } = {
|
||||
sortKey: initialSortKey,
|
||||
|
||||
@@ -129,10 +129,8 @@ class SeriesDetailsSeason extends Component {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const expand = _.some(items, (item) => {
|
||||
return isAfter(item.airDateUtc) ||
|
||||
isAfter(item.airDateUtc, { days: -30 });
|
||||
});
|
||||
const expand = _.some(items, (item) => isAfter(item.airDateUtc) || isAfter(item.airDateUtc, { days: -30 })) ||
|
||||
items.every((item) => !item.airDateUtc);
|
||||
|
||||
onExpandPress(seasonNumber, expand && seasonNumber > 0);
|
||||
}
|
||||
|
||||
@@ -190,11 +190,15 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
|
||||
|
||||
if (isSmallScreen) {
|
||||
const padding = bodyPaddingSmallScreen - 5;
|
||||
const width = window.innerWidth - padding * 2;
|
||||
const height = window.innerHeight;
|
||||
|
||||
setSize({
|
||||
width: window.innerWidth - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
if (width !== size.width || height !== size.height) {
|
||||
setSize({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -202,13 +206,18 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
|
||||
if (current) {
|
||||
const width = current.clientWidth;
|
||||
const padding = bodyPadding - 5;
|
||||
const finalWidth = width - padding * 2;
|
||||
|
||||
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
width: finalWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
|
||||
@@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) {
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
fields && fields.some((x) => x.label === translate('RegularExpression')) &&
|
||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||
|
||||
@@ -133,7 +133,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('PriorityHelpText')}
|
||||
helpText={translate('DownloadClientPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
|
||||
@@ -10,6 +10,7 @@ import translate from 'Utilities/String/translate';
|
||||
import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
|
||||
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
||||
import ImportListOptions from './Options/ImportListOptions';
|
||||
|
||||
class ImportListSettings extends Component {
|
||||
|
||||
@@ -19,7 +20,10 @@ class ImportListSettings extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._saveCallback = null;
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false,
|
||||
isManageImportListsOpen: false
|
||||
};
|
||||
@@ -28,6 +32,14 @@ class ImportListSettings extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
setChildSave = (saveCallback) => {
|
||||
this._saveCallback = saveCallback;
|
||||
};
|
||||
|
||||
onChildStateChange = (payload) => {
|
||||
this.setState(payload);
|
||||
};
|
||||
|
||||
setListOptionsRef = (ref) => {
|
||||
this._listOptions = ref;
|
||||
};
|
||||
@@ -47,7 +59,9 @@ class ImportListSettings extends Component {
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this._listOptions.getWrappedInstance().save();
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
@@ -93,6 +107,12 @@ class ImportListSettings extends Component {
|
||||
|
||||
<PageContentBody>
|
||||
<ImportListsConnector />
|
||||
|
||||
<ImportListOptions
|
||||
setChildSave={this.setChildSave}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
|
||||
<ImportListsExclusionsConnector />
|
||||
<ManageImportListsModal
|
||||
isOpen={isManageImportListsOpen}
|
||||
|
||||
148
frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx
Normal file
148
frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchImportListOptions,
|
||||
saveImportListOptions,
|
||||
setImportListOptionsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const SECTION = 'importListOptions';
|
||||
const cleanLibraryLevelOptions = [
|
||||
{ key: 'disabled', value: () => translate('Disabled') },
|
||||
{ key: 'logOnly', value: () => translate('LogOnly') },
|
||||
{ key: 'keepAndUnmonitor', value: () => translate('KeepAndUnmonitorSeries') },
|
||||
{ key: 'keepAndTag', value: () => translate('KeepAndTagSeries') },
|
||||
];
|
||||
|
||||
function createImportListOptionsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.advancedSettings,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
save: sectionSettings.isSaving,
|
||||
...sectionSettings,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface ImportListOptionsPageProps {
|
||||
setChildSave(saveCallback: () => void): void;
|
||||
onChildStateChange(payload: unknown): void;
|
||||
}
|
||||
|
||||
function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
const { setChildSave, onChildStateChange } = props;
|
||||
const selected = useSelector(createImportListOptionsSelector());
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
} = selected;
|
||||
|
||||
const { listSyncLevel, listSyncTag } = settings;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
// @ts-expect-error 'setImportListOptionsValue' isn't typed yet
|
||||
dispatch(setImportListOptionsValue({ name, value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onTagChange = useCallback(
|
||||
({ name, value }: { name: string; value: number[] }) => {
|
||||
const id = value.length === 0 ? 0 : value.pop();
|
||||
// @ts-expect-error 'setImportListOptionsValue' isn't typed yet
|
||||
dispatch(setImportListOptionsValue({ name, value: id }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchImportListOptions());
|
||||
setChildSave(() => dispatch(saveImportListOptions()));
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges({ section: SECTION }));
|
||||
};
|
||||
}, [dispatch, setChildSave]);
|
||||
|
||||
useEffect(() => {
|
||||
onChildStateChange({
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
});
|
||||
}, [onChildStateChange, isSaving, hasPendingChanges]);
|
||||
|
||||
const translatedLevelOptions = cleanLibraryLevelOptions.map(
|
||||
({ key, value }) => {
|
||||
return {
|
||||
key,
|
||||
value: value(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return advancedSettings ? (
|
||||
<FieldSet legend={translate('Options')}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<div>{translate('UnableToLoadListOptions')}</div>
|
||||
) : null}
|
||||
|
||||
{hasSettings && !isFetching && !error ? (
|
||||
<Form>
|
||||
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('CleanLibraryLevel')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="listSyncLevel"
|
||||
values={translatedLevelOptions}
|
||||
helpText={translate('ListSyncLevelHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...listSyncLevel}
|
||||
/>
|
||||
</FormGroup>
|
||||
{listSyncLevel.value === 'keepAndTag' ? (
|
||||
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('ListSyncTag')}</FormLabel>
|
||||
<FormInputGroup
|
||||
{...listSyncTag}
|
||||
type={inputTypes.TAG}
|
||||
name="listSyncTag"
|
||||
value={listSyncTag.value === 0 ? [] : [listSyncTag.value]}
|
||||
helpText={translate('ListSyncTagHelpText')}
|
||||
onChange={onTagChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
</Form>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default ImportListOptions;
|
||||
@@ -191,7 +191,7 @@ class Notification extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
|
||||
@@ -15,12 +15,17 @@ function PendingChangesModal(props) {
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
bindShortcut
|
||||
bindShortcut,
|
||||
unbindShortcut
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut('enter', onConfirm);
|
||||
}, [bindShortcut, onConfirm]);
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -61,7 +66,8 @@ PendingChangesModal.propTypes = {
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
bindShortcut: PropTypes.func.isRequired,
|
||||
unbindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
PendingChangesModal.defaultProps = {
|
||||
|
||||
@@ -88,7 +88,7 @@ function EditDelayProfileModalContent(props) {
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<div>
|
||||
{translate('AddQualityProfileError')}
|
||||
{translate('AddDelayProfileError')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -94,7 +94,12 @@ export default {
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.DESCENDING
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
|
||||
64
frontend/src/Store/Actions/Settings/importListOptions.js
Normal file
64
frontend/src/Store/Actions/Settings/importListOptions.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createSaveHandler from 'Store/Actions/Creators/createSaveHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.importListOptions';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_IMPORT_LIST_OPTIONS = 'settings/importListOptions/fetchImportListOptions';
|
||||
export const SAVE_IMPORT_LIST_OPTIONS = 'settings/importListOptions/saveImportListOptions';
|
||||
export const SET_IMPORT_LIST_OPTIONS_VALUE = 'settings/importListOptions/setImportListOptionsValue';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchImportListOptions = createThunk(FETCH_IMPORT_LIST_OPTIONS);
|
||||
export const saveImportListOptions = createThunk(SAVE_IMPORT_LIST_OPTIONS);
|
||||
export const setImportListOptionsValue = createAction(SET_IMPORT_LIST_OPTIONS_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
pendingChanges: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
item: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_IMPORT_LIST_OPTIONS]: createFetchHandler(section, '/config/importlist'),
|
||||
[SAVE_IMPORT_LIST_OPTIONS]: createSaveHandler(section, '/config/importlist')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_IMPORT_LIST_OPTIONS_VALUE]: createSetSettingValueReducer(section)
|
||||
}
|
||||
|
||||
};
|
||||
@@ -99,7 +99,12 @@ export default {
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.DESCENDING
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
@@ -149,7 +154,13 @@ export default {
|
||||
delete selectedSchema.name;
|
||||
|
||||
selectedSchema.fields = selectedSchema.fields.map((field) => {
|
||||
return { ...field };
|
||||
const newField = { ...field };
|
||||
|
||||
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
|
||||
newField.value = '';
|
||||
}
|
||||
|
||||
return newField;
|
||||
});
|
||||
|
||||
newState.selectedSchema = selectedSchema;
|
||||
|
||||
@@ -158,6 +158,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'added',
|
||||
label: () => translate('Added'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'progress',
|
||||
label: () => translate('Progress'),
|
||||
@@ -424,13 +430,14 @@ export const actionHandlers = handleThunks({
|
||||
id,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(updateItem({ section: paged, id, isRemoving: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
||||
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||
method: 'DELETE'
|
||||
}).request;
|
||||
|
||||
@@ -448,7 +455,8 @@ export const actionHandlers = handleThunks({
|
||||
ids,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(batchActions([
|
||||
@@ -464,7 +472,7 @@ export const actionHandlers = handleThunks({
|
||||
]));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
||||
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
|
||||
@@ -10,6 +10,7 @@ import downloadClientOptions from './Settings/downloadClientOptions';
|
||||
import downloadClients from './Settings/downloadClients';
|
||||
import general from './Settings/general';
|
||||
import importListExclusions from './Settings/importListExclusions';
|
||||
import importListOptions from './Settings/importListOptions';
|
||||
import importLists from './Settings/importLists';
|
||||
import indexerOptions from './Settings/indexerOptions';
|
||||
import indexers from './Settings/indexers';
|
||||
@@ -33,6 +34,7 @@ export * from './Settings/delayProfiles';
|
||||
export * from './Settings/downloadClients';
|
||||
export * from './Settings/downloadClientOptions';
|
||||
export * from './Settings/general';
|
||||
export * from './Settings/importListOptions';
|
||||
export * from './Settings/importLists';
|
||||
export * from './Settings/importListExclusions';
|
||||
export * from './Settings/indexerOptions';
|
||||
@@ -69,6 +71,7 @@ export const defaultState = {
|
||||
general: general.defaultState,
|
||||
importLists: importLists.defaultState,
|
||||
importListExclusions: importListExclusions.defaultState,
|
||||
importListOptions: importListOptions.defaultState,
|
||||
indexerOptions: indexerOptions.defaultState,
|
||||
indexers: indexers.defaultState,
|
||||
languages: languages.defaultState,
|
||||
@@ -112,6 +115,7 @@ export const actionHandlers = handleThunks({
|
||||
...general.actionHandlers,
|
||||
...importLists.actionHandlers,
|
||||
...importListExclusions.actionHandlers,
|
||||
...importListOptions.actionHandlers,
|
||||
...indexerOptions.actionHandlers,
|
||||
...indexers.actionHandlers,
|
||||
...languages.actionHandlers,
|
||||
@@ -146,6 +150,7 @@ export const reducers = createHandleActions({
|
||||
...general.reducers,
|
||||
...importLists.reducers,
|
||||
...importListExclusions.reducers,
|
||||
...importListOptions.reducers,
|
||||
...indexerOptions.reducers,
|
||||
...indexers.reducers,
|
||||
...languages.reducers,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createDimensionsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.app.dimensions,
|
||||
(state: AppState) => state.app.dimensions,
|
||||
(dimensions) => {
|
||||
return dimensions;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
|
||||
function createSettingsSectionSelector(section) {
|
||||
return createSelector(
|
||||
(state) => state.settings[section],
|
||||
(sectionSettings) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
item,
|
||||
pendingChanges,
|
||||
isSaving,
|
||||
saveError
|
||||
} = sectionSettings;
|
||||
|
||||
const settings = selectSettings(item, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createSettingsSectionSelector;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppSectionState, {
|
||||
AppSectionItemState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import AppState from 'App/State/AppState';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { PendingSection } from 'typings/pending';
|
||||
|
||||
type SettingNames = keyof Omit<AppState['settings'], 'advancedSettings'>;
|
||||
type GetSectionState<Name extends SettingNames> = AppState['settings'][Name];
|
||||
type GetSettingsSectionItemType<Name extends SettingNames> =
|
||||
GetSectionState<Name> extends AppSectionItemState<infer R>
|
||||
? R
|
||||
: GetSectionState<Name> extends AppSectionState<infer R>
|
||||
? R
|
||||
: never;
|
||||
|
||||
type AppStateWithPending<Name extends SettingNames> = {
|
||||
item?: GetSettingsSectionItemType<Name>;
|
||||
pendingChanges?: Partial<GetSettingsSectionItemType<Name>>;
|
||||
saveError?: Error;
|
||||
} & GetSectionState<Name>;
|
||||
|
||||
function createSettingsSectionSelector<Name extends SettingNames>(
|
||||
section: Name
|
||||
) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings[section],
|
||||
(sectionSettings) => {
|
||||
const { item, pendingChanges, saveError, ...other } =
|
||||
sectionSettings as AppStateWithPending<Name>;
|
||||
|
||||
const { settings, ...rest } = selectSettings(
|
||||
item,
|
||||
pendingChanges,
|
||||
saveError
|
||||
);
|
||||
|
||||
return {
|
||||
...other,
|
||||
saveError,
|
||||
settings: settings as PendingSection<GetSettingsSectionItemType<Name>>,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createSettingsSectionSelector;
|
||||
@@ -213,6 +213,8 @@ module.exports = {
|
||||
calendarTextDim: '#eee',
|
||||
calendarTextDimAlternate: '#fff',
|
||||
|
||||
calendarFullColorFilter: 'grayscale(90%) contrast(200%) saturate(50%)',
|
||||
|
||||
//
|
||||
// Table
|
||||
|
||||
|
||||
@@ -215,6 +215,8 @@ module.exports = {
|
||||
calendarTextDim: '#666',
|
||||
calendarTextDimAlternate: '#242424',
|
||||
|
||||
calendarFullColorFilter: 'brightness(30%)',
|
||||
|
||||
//
|
||||
// Table
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class About extends Component {
|
||||
runtimeVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -76,6 +77,11 @@ class About extends Component {
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DatabaseMigration')}
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
@@ -117,6 +123,7 @@ About.propTypes = {
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
|
||||
10
frontend/src/typings/ImportListOptionsSettings.ts
Normal file
10
frontend/src/typings/ImportListOptionsSettings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type ListSyncLevel =
|
||||
| 'disabled'
|
||||
| 'logOnly'
|
||||
| 'keepAndUnmonitor'
|
||||
| 'keepAndTag';
|
||||
|
||||
export default interface ImportListOptionsSettings {
|
||||
listSyncLevel: ListSyncLevel;
|
||||
listSyncTag: number;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ interface Queue extends ModelBase {
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
added?: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState: QueueTrackedDownloadState;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface UiSettings {
|
||||
theme: string;
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
|
||||
9
frontend/src/typings/pending.ts
Normal file
9
frontend/src/typings/pending.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Pending<T> {
|
||||
value: T;
|
||||
errors: any[];
|
||||
warnings: any[];
|
||||
}
|
||||
|
||||
export type PendingSection<T> = {
|
||||
[K in keyof T]: Pending<T[K]>;
|
||||
};
|
||||
@@ -10,6 +10,16 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
|
||||
where TSubject : class, IDiskProvider
|
||||
{
|
||||
[Test]
|
||||
public void writealltext_should_truncate_existing()
|
||||
{
|
||||
var file = GetTempFilePath();
|
||||
|
||||
Subject.WriteAllText(file, "A pretty long string");
|
||||
Subject.WriteAllText(file, "A short string");
|
||||
Subject.ReadAllText(file).Should().Be("A short string");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Retry(5)]
|
||||
public void directory_exist_should_be_able_to_find_existing_folder()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ReverseFixture
|
||||
{
|
||||
[TestCase("input", "tupni")]
|
||||
[TestCase("racecar", "racecar")]
|
||||
public void should_reverse_string(string input, string expected)
|
||||
{
|
||||
input.Reverse().Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,16 @@ namespace NzbDrone.Common.Test.Http
|
||||
response.Content.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_timeout_request()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
|
||||
|
||||
request.RequestTimeout = new TimeSpan(0, 0, 5);
|
||||
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_execute_https_get()
|
||||
{
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
var testPath = Path.Combine(path, "sonarr_write_test.txt");
|
||||
var testContent = $"This file was created to verify if '{path}' is writable. It should've been automatically deleted. Feel free to delete it.";
|
||||
File.WriteAllText(testPath, testContent);
|
||||
WriteAllText(testPath, testContent);
|
||||
File.Delete(testPath);
|
||||
return true;
|
||||
}
|
||||
@@ -311,7 +311,16 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
Ensure.That(filename, () => filename).IsValidPath(PathValidationType.CurrentOs);
|
||||
RemoveReadOnly(filename);
|
||||
File.WriteAllText(filename, contents);
|
||||
|
||||
// File.WriteAllText is broken on net core when writing to some CIFS mounts
|
||||
// This workaround from https://github.com/dotnet/runtime/issues/42790#issuecomment-700362617
|
||||
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
using (var writer = new StreamWriter(fs))
|
||||
{
|
||||
writer.Write(contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void FolderSetLastWriteTime(string path, DateTime dateTime)
|
||||
|
||||
@@ -242,5 +242,14 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
return input.Contains(':') ? $"[{input}]" : input;
|
||||
}
|
||||
|
||||
public static string Reverse(this string text)
|
||||
{
|
||||
var array = text.ToCharArray();
|
||||
|
||||
Array.Reverse(array);
|
||||
|
||||
return new string(array);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,31 +102,38 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
var httpClient = GetClient(request.Url);
|
||||
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
try
|
||||
{
|
||||
byte[] data = null;
|
||||
|
||||
try
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
{
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
byte[] data = null;
|
||||
|
||||
try
|
||||
{
|
||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||
}
|
||||
|
||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||
|
||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||
}
|
||||
|
||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||
|
||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
data = new XElement("base64", Convert.ToBase64String(bytes));
|
||||
}
|
||||
else if (value is Dictionary<string, string> d)
|
||||
{
|
||||
data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value))));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
||||
|
||||
@@ -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.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
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
|
||||
{
|
||||
EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(),
|
||||
Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.English,
|
||||
Language.French
|
||||
},
|
||||
Filename = "Series.Title.S01E01"
|
||||
};
|
||||
}
|
||||
|
||||
[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.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
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
|
||||
{
|
||||
EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(),
|
||||
Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.French
|
||||
},
|
||||
Filename = "Series.Title.S01E01"
|
||||
};
|
||||
}
|
||||
|
||||
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.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
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
|
||||
{
|
||||
EpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build(),
|
||||
Series = Builder<Series>.CreateNew().With(s => s.OriginalLanguage = Language.English).Build(),
|
||||
Size = 100.Megabytes(),
|
||||
Languages = new List<Language>
|
||||
{
|
||||
Language.French
|
||||
},
|
||||
Filename = "Series.Title.S01E01"
|
||||
};
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class parse_title_from_existing_subtitle_filesFixture : MigrationTest<parse_title_from_existing_subtitle_files>
|
||||
{
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle - 3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle - 3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
|
||||
[TestCase("Name (2020) - Name.2020.S01E03.REAL.PROPER.1080p.HEVC.x265-MeGusta - 0609901d2ea34acd81c9030980406065.en.forced.srt", "Name (2020)/Season 1/Name (2020) - Name.2020.S01E03.REAL.PROPER.1080p.HEVC.x265-MeGusta - 0609901d2ea34acd81c9030980406065.mkv", null, 0)]
|
||||
public void should_process_file_with_missing_title(string subtitlePath, string episodePath, string title, int copy)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var db = WithDapperMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("SubtitleFiles").Row(new
|
||||
{
|
||||
SeriesId = 1,
|
||||
SeasonNumber = 1,
|
||||
EpisodeFileId = 1,
|
||||
RelativePath = subtitlePath,
|
||||
Added = now,
|
||||
LastUpdated = now,
|
||||
Extension = Path.GetExtension(subtitlePath),
|
||||
Language = 10,
|
||||
LanguageTags = new List<string> { "sdh" }.ToJson()
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("EpisodeFiles").Row(new
|
||||
{
|
||||
Id = 1,
|
||||
SeriesId = 1,
|
||||
RelativePath = episodePath,
|
||||
Quality = new { }.ToJson(),
|
||||
Size = 0,
|
||||
DateAdded = now,
|
||||
SeasonNumber = 1,
|
||||
Languages = new List<int> { 1 }.ToJson()
|
||||
});
|
||||
});
|
||||
|
||||
var files = db.Query<SubtitleFile198>("SELECT * FROM \"SubtitleFiles\"").ToList();
|
||||
|
||||
files.Should().HaveCount(1);
|
||||
|
||||
files.First().Title.Should().Be(title);
|
||||
files.First().Copy.Should().Be(copy);
|
||||
files.First().LanguageTags.Should().NotContain("sdh");
|
||||
files.First().Language.Should().NotBe(10);
|
||||
}
|
||||
}
|
||||
|
||||
public class SubtitleFile198
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int? EpisodeFileId { get; set; }
|
||||
public int? SeasonNumber { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public string Extension { get; set; }
|
||||
public int Language { get; set; }
|
||||
public int Copy { get; set; }
|
||||
public string Title { get; set; }
|
||||
public List<string> LanguageTags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Notifications.Email;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class email_encryptionFixture : MigrationTest<email_encryption>
|
||||
{
|
||||
[Test]
|
||||
public void should_convert_do_not_require_encryption_to_auto()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Notifications").Row(new
|
||||
{
|
||||
OnGrab = true,
|
||||
OnDownload = true,
|
||||
OnUpgrade = true,
|
||||
OnHealthIssue = true,
|
||||
IncludeHealthWarnings = true,
|
||||
OnRename = true,
|
||||
Name = "Mail Sonarr",
|
||||
Implementation = "Email",
|
||||
Tags = "[]",
|
||||
Settings = new EmailSettings200
|
||||
{
|
||||
Server = "smtp.gmail.com",
|
||||
Port = 563,
|
||||
To = new List<string> { "dont@email.me" },
|
||||
RequireEncryption = false
|
||||
}.ToJson(),
|
||||
ConfigContract = "EmailSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<NotificationDefinition201>("SELECT * FROM \"Notifications\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Email");
|
||||
items.First().ConfigContract.Should().Be("EmailSettings");
|
||||
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_convert_require_encryption_to_always()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Notifications").Row(new
|
||||
{
|
||||
OnGrab = true,
|
||||
OnDownload = true,
|
||||
OnUpgrade = true,
|
||||
OnHealthIssue = true,
|
||||
IncludeHealthWarnings = true,
|
||||
OnRename = true,
|
||||
Name = "Mail Sonarr",
|
||||
Implementation = "Email",
|
||||
Tags = "[]",
|
||||
Settings = new EmailSettings200
|
||||
{
|
||||
Server = "smtp.gmail.com",
|
||||
Port = 563,
|
||||
To = new List<string> { "dont@email.me" },
|
||||
RequireEncryption = true
|
||||
}.ToJson(),
|
||||
ConfigContract = "EmailSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<NotificationDefinition201>("SELECT * FROM \"Notifications\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Email");
|
||||
items.First().ConfigContract.Should().Be("EmailSettings");
|
||||
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Always);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_defaults_when_settings_are_empty()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Notifications").Row(new
|
||||
{
|
||||
OnGrab = true,
|
||||
OnDownload = true,
|
||||
OnUpgrade = true,
|
||||
OnHealthIssue = true,
|
||||
IncludeHealthWarnings = true,
|
||||
OnRename = true,
|
||||
Name = "Mail Sonarr",
|
||||
Implementation = "Email",
|
||||
Tags = "[]",
|
||||
Settings = new { }.ToJson(),
|
||||
ConfigContract = "EmailSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<NotificationDefinition201>("SELECT * FROM \"Notifications\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Implementation.Should().Be("Email");
|
||||
items.First().ConfigContract.Should().Be("EmailSettings");
|
||||
items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred);
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationDefinition201
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public EmailSettings201 Settings { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool OnGrab { get; set; }
|
||||
public bool OnDownload { get; set; }
|
||||
public bool OnUpgrade { get; set; }
|
||||
public bool OnRename { get; set; }
|
||||
public bool OnSeriesDelete { get; set; }
|
||||
public bool OnEpisodeFileDelete { get; set; }
|
||||
public bool OnEpisodeFileDeleteForUpgrade { get; set; }
|
||||
public bool OnHealthIssue { get; set; }
|
||||
public bool OnApplicationUpdate { get; set; }
|
||||
public bool OnManualInteractionRequired { get; set; }
|
||||
public bool OnSeriesAdd { get; set; }
|
||||
public bool OnHealthRestored { get; set; }
|
||||
public bool SupportsOnGrab { get; set; }
|
||||
public bool SupportsOnDownload { get; set; }
|
||||
public bool SupportsOnUpgrade { get; set; }
|
||||
public bool SupportsOnRename { get; set; }
|
||||
public bool SupportsOnSeriesDelete { get; set; }
|
||||
public bool SupportsOnEpisodeFileDelete { get; set; }
|
||||
public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; }
|
||||
public bool SupportsOnHealthIssue { get; set; }
|
||||
public bool IncludeHealthWarnings { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
}
|
||||
|
||||
public class EmailSettings200
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool RequireEncryption { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string From { get; set; }
|
||||
public IEnumerable<string> To { get; set; }
|
||||
public IEnumerable<string> Cc { get; set; }
|
||||
public IEnumerable<string> Bcc { get; set; }
|
||||
}
|
||||
|
||||
public class EmailSettings201
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public int Port { get; set; }
|
||||
public int UseEncryption { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string From { get; set; }
|
||||
public IEnumerable<string> To { get; set; }
|
||||
public IEnumerable<string> Cc { get; set; }
|
||||
public IEnumerable<string> Bcc { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -454,6 +454,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
||||
|
||||
[TestCase("0")]
|
||||
[TestCase("15d")]
|
||||
[TestCase("")]
|
||||
[TestCase(null)]
|
||||
public void should_set_history_removes_completed_downloads_false(string historyRetention)
|
||||
{
|
||||
_config.Misc.history_retention = historyRetention;
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
public void should_return_ok_on_episode_imported_event()
|
||||
{
|
||||
GivenFolderExists(_downloadRootPath);
|
||||
var importEvent = new EpisodeImportedEvent(new LocalEpisode(), new EpisodeFile(), new List<EpisodeFile>(), true, new DownloadClientItem());
|
||||
var importEvent = new EpisodeImportedEvent(new LocalEpisode(), new EpisodeFile(), new List<DeletedEpisodeFile>(), true, new DownloadClientItem());
|
||||
|
||||
Subject.Check(importEvent).ShouldBeOk();
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.HistoryTests
|
||||
DownloadId = "abcd"
|
||||
};
|
||||
|
||||
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, new List<EpisodeFile>(), true, downloadClientItem));
|
||||
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, new List<DeletedEpisodeFile>(), true, downloadClientItem));
|
||||
|
||||
Mocker.GetMock<IHistoryRepository>()
|
||||
.Verify(v => v.Insert(It.Is<EpisodeHistory>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path))));
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.ImportListItems;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class FetchAndParseImportListServiceFixture : CoreTest<FetchAndParseImportListService>
|
||||
{
|
||||
private List<IImportList> _importLists;
|
||||
private List<ImportListItemInfo> _listSeries;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_importLists = new List<IImportList>();
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
|
||||
.Returns(_importLists);
|
||||
|
||||
_listSeries = Builder<ImportListItemInfo>.CreateListOfSize(5)
|
||||
.Build().ToList();
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Setup(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()))
|
||||
.Returns((string value) => new List<Tv.Series>() { new Tv.Series() { ImdbId = value } });
|
||||
}
|
||||
|
||||
private Mock<IImportList> WithList(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult, TimeSpan? minRefresh = null, int? lastSyncOffset = null, int? syncDeletedCount = null)
|
||||
{
|
||||
return CreateListResult(id, enabled, enabledAuto, fetchResult, minRefresh, lastSyncOffset, syncDeletedCount);
|
||||
}
|
||||
|
||||
private Mock<IImportList> CreateListResult(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult, TimeSpan? minRefresh = null, int? lastSyncOffset = null, int? syncDeletedCount = null)
|
||||
{
|
||||
var refreshInterval = minRefresh ?? TimeSpan.FromHours(12);
|
||||
var importListDefinition = new ImportListDefinition { Id = id, Enable = enabled, EnableAutomaticAdd = enabledAuto, MinRefreshInterval = refreshInterval };
|
||||
|
||||
var mockImportList = new Mock<IImportList>();
|
||||
mockImportList.SetupGet(s => s.Definition).Returns(importListDefinition);
|
||||
mockImportList.Setup(s => s.Fetch()).Returns(fetchResult);
|
||||
mockImportList.SetupGet(s => s.MinRefreshInterval).Returns(refreshInterval);
|
||||
|
||||
DateTime? lastSync = lastSyncOffset.HasValue ? DateTime.UtcNow.AddHours(lastSyncOffset.Value) : null;
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Setup(v => v.GetListStatus(id))
|
||||
.Returns(new ImportListStatus() { LastInfoSync = lastSync });
|
||||
|
||||
if (syncDeletedCount.HasValue)
|
||||
{
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Setup(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), id))
|
||||
.Returns(syncDeletedCount.Value);
|
||||
}
|
||||
|
||||
_importLists.Add(mockImportList.Object);
|
||||
|
||||
return mockImportList;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_recently_fetched_list()
|
||||
{
|
||||
var fetchResult = new ImportListFetchResult();
|
||||
var list = WithList(1, true, true, fetchResult, lastSyncOffset: 0);
|
||||
|
||||
var result = Subject.Fetch();
|
||||
|
||||
list.Verify(f => f.Fetch(), Times.Never());
|
||||
result.Series.Count.Should().Be(0);
|
||||
result.AnyFailure.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_recent_and_fetch_good()
|
||||
{
|
||||
var fetchResult = new ImportListFetchResult();
|
||||
var recent = WithList(1, true, true, fetchResult, lastSyncOffset: 0);
|
||||
var old = WithList(2, true, true, fetchResult);
|
||||
|
||||
var result = Subject.Fetch();
|
||||
|
||||
recent.Verify(f => f.Fetch(), Times.Never());
|
||||
old.Verify(f => f.Fetch(), Times.Once());
|
||||
result.AnyFailure.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_failure_if_single_list_fails()
|
||||
{
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(1, true, true, fetchResult);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(It.IsAny<int>(), It.IsAny<bool>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_failure_if_any_list_fails()
|
||||
{
|
||||
var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(1, true, true, fetchResult1);
|
||||
var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(2, true, true, fetchResult2);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_early_if_no_available_lists()
|
||||
{
|
||||
var listResult = Subject.Fetch();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.GetListStatus(It.IsAny<int>()), Times.Never());
|
||||
|
||||
listResult.Series.Count.Should().Be(0);
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_store_series_if_list_doesnt_fail()
|
||||
{
|
||||
var listId = 1;
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(listId, true, true, fetchResult);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(listId, false), Times.Once());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(_listSeries, listId), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_store_series_if_list_fails()
|
||||
{
|
||||
var listId = 1;
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(listId, true, true, fetchResult);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(listId, false), Times.Never());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), listId), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_only_store_series_for_lists_that_dont_fail()
|
||||
{
|
||||
var passedListId = 1;
|
||||
var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(passedListId, true, true, fetchResult1);
|
||||
var failedListId = 2;
|
||||
var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = true };
|
||||
WithList(failedListId, true, true, fetchResult2);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(passedListId, false), Times.Once());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(_listSeries, passedListId), Times.Once());
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(failedListId, false), Times.Never());
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Verify(v => v.SyncSeriesForList(It.IsAny<List<ImportListItemInfo>>(), failedListId), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_all_results_for_all_lists()
|
||||
{
|
||||
var passedListId = 1;
|
||||
var fetchResult1 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(passedListId, true, true, fetchResult1);
|
||||
var secondListId = 2;
|
||||
var fetchResult2 = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(secondListId, true, true, fetchResult2);
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
listResult.Series.Count.Should().Be(5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_removed_flag_if_list_has_removed_items()
|
||||
{
|
||||
var listId = 1;
|
||||
var fetchResult = new ImportListFetchResult { Series = _listSeries, AnyFailure = false };
|
||||
WithList(listId, true, true, fetchResult, syncDeletedCount: 500);
|
||||
|
||||
var result = Subject.Fetch();
|
||||
result.AnyFailure.Should().BeFalse();
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Verify(v => v.UpdateListSyncStatus(listId, true), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.ImportLists.ImportListItems;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
public class ImportListItemServiceFixture : CoreTest<ImportListItemService>
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var existing = Builder<ImportListItemInfo>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.With(s => s.ImdbId = "6")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.With(s => s.ImdbId = "7")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 8)
|
||||
.With(s => s.ImdbId = "8")
|
||||
.Build().ToList();
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Setup(v => v.GetAllForLists(It.IsAny<List<int>>()))
|
||||
.Returns(existing);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_insert_new_update_existing_and_delete_missing()
|
||||
{
|
||||
var newItems = Builder<ImportListItemInfo>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 5)
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.Build().ToList();
|
||||
|
||||
var numDeleted = Subject.SyncSeriesForList(newItems, 1);
|
||||
|
||||
numDeleted.Should().Be(1);
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Verify(v => v.InsertMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 5)), Times.Once());
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Verify(v => v.UpdateMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 2 && s[0].TvdbId == 6 && s[1].TvdbId == 7)), Times.Once());
|
||||
Mocker.GetMock<IImportListItemInfoRepository>()
|
||||
.Verify(v => v.DeleteMany(It.Is<List<ImportListItemInfo>>(s => s.Count == 1 && s[0].TvdbId == 8)), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.Exclusions;
|
||||
using NzbDrone.Core.ImportLists.ImportListItems;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -13,17 +17,61 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
public class ImportListSyncServiceFixture : CoreTest<ImportListSyncService>
|
||||
{
|
||||
private List<ImportListItemInfo> _importListReports;
|
||||
private ImportListFetchResult _importListFetch;
|
||||
private List<ImportListItemInfo> _list1Series;
|
||||
private List<ImportListItemInfo> _list2Series;
|
||||
|
||||
private List<Series> _existingSeries;
|
||||
private List<IImportList> _importLists;
|
||||
private ImportListSyncCommand _commandAll;
|
||||
private ImportListSyncCommand _commandSingle;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var importListItem1 = new ImportListItemInfo
|
||||
_importLists = new List<IImportList>();
|
||||
|
||||
var item1 = new ImportListItemInfo()
|
||||
{
|
||||
Title = "Breaking Bad"
|
||||
};
|
||||
|
||||
_importListReports = new List<ImportListItemInfo> { importListItem1 };
|
||||
_list1Series = new List<ImportListItemInfo>() { item1 };
|
||||
|
||||
_existingSeries = Builder<Series>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.With(s => s.ImdbId = "6")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.With(s => s.ImdbId = "7")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 8)
|
||||
.With(s => s.ImdbId = "8")
|
||||
.Build().ToList();
|
||||
|
||||
_list2Series = Builder<ImportListItemInfo>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(s => s.TvdbId = 6)
|
||||
.With(s => s.ImdbId = "6")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 7)
|
||||
.With(s => s.ImdbId = "7")
|
||||
.TheNext(1)
|
||||
.With(s => s.TvdbId = 8)
|
||||
.With(s => s.ImdbId = "8")
|
||||
.Build().ToList();
|
||||
|
||||
_importListFetch = new ImportListFetchResult(_list1Series, false);
|
||||
|
||||
_commandAll = new ImportListSyncCommand
|
||||
{
|
||||
};
|
||||
|
||||
_commandSingle = new ImportListSyncCommand
|
||||
{
|
||||
DefinitionId = 1
|
||||
};
|
||||
|
||||
var mockImportList = new Mock<IImportList>();
|
||||
|
||||
@@ -31,6 +79,10 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
.Setup(v => v.AllSeriesTvdbIds())
|
||||
.Returns(new List<int>());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(v => v.GetAllSeries())
|
||||
.Returns(_existingSeries);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Setup(v => v.SearchForNewSeries(It.IsAny<string>()))
|
||||
.Returns(new List<Series>());
|
||||
@@ -41,15 +93,19 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<ImportListDefinition> { new ImportListDefinition { ShouldMonitor = MonitorTypes.All } });
|
||||
.Returns(() => _importLists.Select(x => x.Definition as ImportListDefinition).ToList());
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.GetAvailableProviders())
|
||||
.Returns(_importLists);
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.AutomaticAddEnabled(It.IsAny<bool>()))
|
||||
.Returns(new List<IImportList> { mockImportList.Object });
|
||||
.Returns(() => _importLists.Where(x => (x.Definition as ImportListDefinition).EnableAutomaticAdd).ToList());
|
||||
|
||||
Mocker.GetMock<IFetchAndParseImportList>()
|
||||
.Setup(v => v.Fetch())
|
||||
.Returns(_importListReports);
|
||||
.Returns(_importListFetch);
|
||||
|
||||
Mocker.GetMock<IImportListExclusionService>()
|
||||
.Setup(v => v.All())
|
||||
@@ -58,19 +114,19 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
private void WithTvdbId()
|
||||
{
|
||||
_importListReports.First().TvdbId = 81189;
|
||||
_list1Series.First().TvdbId = 81189;
|
||||
}
|
||||
|
||||
private void WithImdbId()
|
||||
{
|
||||
_importListReports.First().ImdbId = "tt0496424";
|
||||
_list1Series.First().ImdbId = "tt0496424";
|
||||
}
|
||||
|
||||
private void WithExistingSeries()
|
||||
{
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(v => v.AllSeriesTvdbIds())
|
||||
.Returns(new List<int> { _importListReports.First().TvdbId });
|
||||
.Returns(new List<int> { _list1Series.First().TvdbId });
|
||||
}
|
||||
|
||||
private void WithExcludedSeries()
|
||||
@@ -81,22 +137,281 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
new ImportListExclusion
|
||||
{
|
||||
TvdbId = 81189
|
||||
TvdbId = _list1Series.First().TvdbId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void WithMonitorType(MonitorTypes monitor)
|
||||
{
|
||||
_importLists.ForEach(li => (li.Definition as ImportListDefinition).ShouldMonitor = monitor);
|
||||
}
|
||||
|
||||
private void WithCleanLevel(ListSyncLevelType cleanLevel, int? tagId = null)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.SetupGet(v => v.ListSyncLevel)
|
||||
.Returns(cleanLevel);
|
||||
if (tagId.HasValue)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.SetupGet(v => v.ListSyncTag)
|
||||
.Returns(tagId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void WithList(int id, bool enabledAuto, int lastSyncHoursOffset = 0, bool pendingRemovals = true, DateTime? disabledTill = null)
|
||||
{
|
||||
var importListDefinition = new ImportListDefinition { Id = id, EnableAutomaticAdd = enabledAuto };
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<ImportListDefinition> { new ImportListDefinition { ShouldMonitor = monitor } });
|
||||
.Setup(v => v.Get(id))
|
||||
.Returns(importListDefinition);
|
||||
|
||||
var mockImportList = new Mock<IImportList>();
|
||||
mockImportList.SetupGet(s => s.Definition).Returns(importListDefinition);
|
||||
mockImportList.SetupGet(s => s.MinRefreshInterval).Returns(TimeSpan.FromHours(12));
|
||||
|
||||
var status = new ImportListStatus()
|
||||
{
|
||||
LastInfoSync = DateTime.UtcNow.AddHours(lastSyncHoursOffset),
|
||||
HasRemovedItemSinceLastClean = pendingRemovals,
|
||||
DisabledTill = disabledTill
|
||||
};
|
||||
|
||||
if (disabledTill.HasValue)
|
||||
{
|
||||
_importListFetch.AnyFailure = true;
|
||||
}
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
.Setup(v => v.GetListStatus(id))
|
||||
.Returns(status);
|
||||
|
||||
_importLists.Add(mockImportList.Object);
|
||||
}
|
||||
|
||||
private void VerifyDidAddTag(int expectedSeriesCount, int expectedTagId)
|
||||
{
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(x => x.Count == expectedSeriesCount && x.All(series => series.Tags.Contains(expectedTagId))), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_library_if_lists_have_not_removed_any_items()
|
||||
{
|
||||
_importListFetch.Series = _existingSeries.Select(x => new ImportListItemInfo() { TvdbId = x.TvdbId }).ToList();
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true, pendingRemovals: false);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.IsAny<List<Series>>(), true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_library_if_config_value_disable()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(new List<Series>(), true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_log_only_on_clean_library_if_config_value_logonly()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.LogOnly);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Once());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(new List<Series>(), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_unmonitor_on_clean_library_if_config_value_keepAndUnmonitor()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
var monitored = _existingSeries.Count(x => x.Monitored);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Once());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count == monitored && s.All(m => !m.Monitored)), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_on_clean_library_if_tvdb_match()
|
||||
{
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Setup(v => v.Exists(6, It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_on_clean_library_if_imdb_match()
|
||||
{
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
|
||||
var x = _importLists;
|
||||
|
||||
Mocker.GetMock<IImportListItemService>()
|
||||
.Setup(v => v.Exists(It.IsAny<int>(), "6"))
|
||||
.Returns(true);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<List<Series>>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_tag_series_on_clean_library_if_config_value_keepAndTag()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.KeepAndTag, 1);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Once());
|
||||
|
||||
VerifyDidAddTag(_existingSeries.Count, 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_clean_if_list_failures()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true, disabledTill: DateTime.UtcNow.AddHours(1));
|
||||
WithCleanLevel(ListSyncLevelType.LogOnly);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.GetAllSeries(), Times.Never());
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.IsAny<List<Series>>(), It.IsAny<bool>()), Times.Never());
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.DeleteSeries(It.IsAny<List<int>>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_new_series_from_single_list_to_library()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 1), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_new_series_from_multiple_list_to_library()
|
||||
{
|
||||
_list2Series.ForEach(m => m.ImportListId = 2);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
|
||||
WithList(1, true);
|
||||
WithList(2, true);
|
||||
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 4), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_new_series_to_library_only_from_enabled_lists()
|
||||
{
|
||||
_list2Series.ForEach(m => m.ImportListId = 2);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
|
||||
WithList(1, true);
|
||||
WithList(2, false);
|
||||
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 1), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_add_duplicate_series_from_seperate_lists()
|
||||
{
|
||||
_list2Series.ForEach(m => m.ImportListId = 2);
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
_importListFetch.Series[0].TvdbId = 6;
|
||||
|
||||
WithList(1, true);
|
||||
WithList(2, true);
|
||||
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(s => s.Count == 3), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_if_series_title_and_no_series_id()
|
||||
{
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Once());
|
||||
@@ -105,8 +420,10 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[Test]
|
||||
public void should_not_search_if_series_title_and_series_id()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Never());
|
||||
@@ -115,8 +432,10 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[Test]
|
||||
public void should_search_by_imdb_if_series_title_and_series_imdb()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithImdbId();
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()), Times.Once());
|
||||
@@ -125,10 +444,12 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[Test]
|
||||
public void should_not_add_if_existing_series()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
WithExistingSeries();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>()));
|
||||
@@ -138,10 +459,12 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[TestCase(MonitorTypes.All, true)]
|
||||
public void should_add_if_not_existing_series(MonitorTypes monitor, bool expectedSeriesMonitored)
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
WithMonitorType(monitor);
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1 && t.First().Monitored == expectedSeriesMonitored), It.IsAny<bool>()));
|
||||
@@ -150,10 +473,12 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
[Test]
|
||||
public void should_not_add_if_excluded_series()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithTvdbId();
|
||||
WithExcludedSeries();
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>()));
|
||||
@@ -177,7 +502,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
{
|
||||
Mocker.GetMock<IFetchAndParseImportList>()
|
||||
.Setup(v => v.Fetch())
|
||||
.Returns(new List<ImportListItemInfo>());
|
||||
.Returns(new ImportListFetchResult());
|
||||
|
||||
Subject.Execute(new ImportListSyncCommand());
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, recentFeed)));
|
||||
|
||||
var releases = await Subject.FetchRecent();
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Path.Combine(_series.Path, "Scenes", "file6.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Shorts", "file7.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Trailers", "file8.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Other", "file9.mkv").AsOsAgnostic(),
|
||||
Path.Combine(_series.Path, "Series Title S01E01 (1080p BluRay x265 10bit Tigole).mkv").AsOsAgnostic(),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
||||
{
|
||||
[TestFixture]
|
||||
public class AggregateSubtitleInfoFixture : CoreTest<AggregateSubtitleInfo>
|
||||
{
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
|
||||
public void should_do_basic_parse(string relativePath, string originalFilePath, string path)
|
||||
{
|
||||
var episodeFile = new EpisodeFile
|
||||
{
|
||||
RelativePath = relativePath,
|
||||
OriginalFilePath = originalFilePath
|
||||
};
|
||||
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
|
||||
|
||||
subtitleTitleInfo.Title.Should().BeNull();
|
||||
subtitleTitleInfo.Copy.Should().Be(0);
|
||||
}
|
||||
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass")]
|
||||
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].testtitle.eng.default.ass")]
|
||||
public void should_not_parse_default(string relativePath, string path)
|
||||
{
|
||||
var episodeFile = new EpisodeFile
|
||||
{
|
||||
RelativePath = relativePath
|
||||
};
|
||||
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
|
||||
|
||||
subtitleTitleInfo.LanguageTags.Should().NotContain("default");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.NotificationTests.EmailTests
|
||||
_emailSettings = Builder<EmailSettings>.CreateNew()
|
||||
.With(s => s.Server = "someserver")
|
||||
.With(s => s.Port = 567)
|
||||
.With(s => s.RequireEncryption = true)
|
||||
.With(s => s.UseEncryption = (int)EmailEncryptionType.Always)
|
||||
.With(s => s.From = "dont@email.me")
|
||||
.With(s => s.To = new string[] { "dont@email.me" })
|
||||
.Build();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
@@ -33,16 +33,16 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
RelativePath = "file1.S01E01E02.mkv"
|
||||
},
|
||||
|
||||
OldFiles = new List<EpisodeFile>
|
||||
OldFiles = new List<DeletedEpisodeFile>
|
||||
{
|
||||
new EpisodeFile
|
||||
new DeletedEpisodeFile(new EpisodeFile
|
||||
{
|
||||
RelativePath = "file1.S01E01.mkv"
|
||||
},
|
||||
new EpisodeFile
|
||||
}, null),
|
||||
new DeletedEpisodeFile(new EpisodeFile
|
||||
{
|
||||
RelativePath = "file1.S01E02.mkv"
|
||||
}
|
||||
}, null)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
|
||||
_downloadMessage = Builder<DownloadMessage>.CreateNew()
|
||||
.With(d => d.Series = series)
|
||||
.With(d => d.EpisodeFile = episodeFile)
|
||||
.With(d => d.OldFiles = new List<EpisodeFile>())
|
||||
.With(d => d.OldFiles = new List<DeletedEpisodeFile>())
|
||||
.Build();
|
||||
|
||||
Subject.Definition = new NotificationDefinition();
|
||||
@@ -40,9 +40,12 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
|
||||
|
||||
private void GivenOldFiles()
|
||||
{
|
||||
_downloadMessage.OldFiles = Builder<EpisodeFile>.CreateListOfSize(1)
|
||||
.Build()
|
||||
.ToList();
|
||||
_downloadMessage.OldFiles = Builder<DeletedEpisodeFile>
|
||||
.CreateListOfSize(1)
|
||||
.All()
|
||||
.WithFactory(() => new DeletedEpisodeFile(Builder<EpisodeFile>.CreateNew().Build(), null))
|
||||
.Build()
|
||||
.ToList();
|
||||
|
||||
Subject.Definition.Settings = new XbmcSettings
|
||||
{
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class TruncatedReleaseGroupFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Series _series;
|
||||
private List<Episode> _episodes;
|
||||
private EpisodeFile _episodeFile;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "Series Title")
|
||||
.Build();
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.MultiEpisodeStyle = 0;
|
||||
_namingConfig.RenameEpisodes = true;
|
||||
|
||||
Mocker.GetMock<INamingConfigService>()
|
||||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
_episodes = new List<Episode>
|
||||
{
|
||||
Builder<Episode>.CreateNew()
|
||||
.With(e => e.Title = "Episode Title 1")
|
||||
.With(e => e.SeasonNumber = 1)
|
||||
.With(e => e.EpisodeNumber = 1)
|
||||
.Build()
|
||||
};
|
||||
|
||||
_episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" };
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
private void GivenProper()
|
||||
{
|
||||
_episodeFile.Quality.Revision.Version = 2;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_truncate_from_beginning()
|
||||
{
|
||||
_series.Title = "The Fantastic Life of Mr. Sisko";
|
||||
|
||||
_episodeFile.Quality.Quality = Quality.Bluray1080p;
|
||||
_episodeFile.ReleaseGroup = "IWishIWasALittleBitTallerIWishIWasABallerIWishIHadAGirlWhoLookedGoodIWouldCallHerIWishIHadARabbitInAHatWithABatAndASixFourImpala";
|
||||
_episodes = _episodes.Take(1).ToList();
|
||||
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}-{ReleaseGroup:12}";
|
||||
|
||||
var result = Subject.BuildFileName(_episodes, _series, _episodeFile, ".mkv");
|
||||
result.Length.Should().BeLessOrEqualTo(255);
|
||||
result.Should().Be("The Fantastic Life of Mr. Sisko - S01E01 - Episode Title 1 Bluray-1080p-IWishIWas....mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_truncate_from_from_end()
|
||||
{
|
||||
_series.Title = "The Fantastic Life of Mr. Sisko";
|
||||
|
||||
_episodeFile.Quality.Quality = Quality.Bluray1080p;
|
||||
_episodeFile.ReleaseGroup = "IWishIWasALittleBitTallerIWishIWasABallerIWishIHadAGirlWhoLookedGoodIWouldCallHerIWishIHadARabbitInAHatWithABatAndASixFourImpala";
|
||||
_episodes = _episodes.Take(1).ToList();
|
||||
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}-{ReleaseGroup:-17}";
|
||||
|
||||
var result = Subject.BuildFileName(_episodes, _series, _episodeFile, ".mkv");
|
||||
result.Length.Should().BeLessOrEqualTo(255);
|
||||
result.Should().Be("The Fantastic Life of Mr. Sisko - S01E01 - Episode Title 1 Bluray-1080p-...ASixFourImpala.mkv");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class TruncatedSeriesTitleFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Series _series;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "Series Title")
|
||||
.Build();
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.MultiEpisodeStyle = 0;
|
||||
_namingConfig.RenameEpisodes = true;
|
||||
|
||||
Mocker.GetMock<INamingConfigService>()
|
||||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
[TestCase("{Series Title:16}", "The Fantastic...")]
|
||||
[TestCase("{Series TitleThe:17}", "Fantastic Life...")]
|
||||
[TestCase("{Series CleanTitle:-13}", "...Mr. Sisko")]
|
||||
public void should_truncate_series_title(string format, string expected)
|
||||
{
|
||||
_series.Title = "The Fantastic Life of Mr. Sisko";
|
||||
_namingConfig.SeriesFolderFormat = format;
|
||||
|
||||
var result = Subject.GetSeriesFolder(_series, _namingConfig);
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,10 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Different show 1. Bölüm (23.10.2023) 720p WebDL AAC H.264 - TURG", "Different show", 1, 0, 0)]
|
||||
[TestCase("Dubbed show 79.BLM Sezon Finali(25.06.2023) 720p WEB-DL AAC2.0 H.264-TURG", "Dubbed show", 79, 0, 0)]
|
||||
[TestCase("Exclusive BLM Documentary with no false positives EP03.1080p.AAC.x264", "Exclusive BLM Documentary with no false positives", 3, 0, 0)]
|
||||
[TestCase("[SubsPlease] Title de Series S2 - 03 (540p) [63501322]", "Title de Series S2", 3, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Dr Series S3 - 21 [1080p]", "Dr Series S3", 21, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Series Title - 12 [1080p].mkv", "Series Title", 12, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)]
|
||||
|
||||
// [TestCase("", "", 0, 0, 0)]
|
||||
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
||||
|
||||
@@ -428,5 +428,38 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
result.Languages.Should().Contain(Language.Original);
|
||||
result.Languages.Should().Contain(Language.English);
|
||||
}
|
||||
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", new[] { "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", new[] { "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", new[] { "forced" }, "testtitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", new[] { "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
|
||||
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
|
||||
{
|
||||
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle);
|
||||
|
||||
subtitleTitleInfo.LanguageTags.Should().BeEquivalentTo(expectedTags);
|
||||
subtitleTitleInfo.Title.Should().BeEquivalentTo(expectedTitle);
|
||||
subtitleTitleInfo.Language.Should().BeEquivalentTo((Language)expectedLanguage);
|
||||
}
|
||||
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ass")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass")]
|
||||
public void should_not_parse_false_title(string postTitle)
|
||||
{
|
||||
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle);
|
||||
subtitleTitleInfo.Language.Should().Be(Language.Unknown);
|
||||
subtitleTitleInfo.LanguageTags.Should().BeEmpty();
|
||||
subtitleTitleInfo.RawTitle.Should().BeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Series Title (2012) - S01E01 - Episode 1 (1080p BluRay x265 r00t).mkv", "r00t")]
|
||||
[TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")]
|
||||
[TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")]
|
||||
[TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")]
|
||||
public void should_parse_exception_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Genre";
|
||||
|
||||
[FieldDefinition(1, Label = "Genre(s)", Type = FieldType.Tag)]
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationGenre", Type = FieldType.Tag)]
|
||||
public IEnumerable<string> Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Series series)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Original Language";
|
||||
|
||||
[FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))]
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationOriginalLanguage", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Series series)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Quality Profile";
|
||||
|
||||
[FieldDefinition(1, Label = "Quality Profile", Type = FieldType.QualityProfile)]
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationQualityProfile", Type = FieldType.QualityProfile)]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Series series)
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Root Folder";
|
||||
|
||||
[FieldDefinition(1, Label = "Root Folder", Type = FieldType.RootFolder)]
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationRootFolder", Type = FieldType.RootFolder)]
|
||||
public string Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Series series)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
public override int Order => 2;
|
||||
public override string ImplementationName => "Series Type";
|
||||
|
||||
[FieldDefinition(1, Label = "Series Type", Type = FieldType.Select, SelectOptions = typeof(SeriesTypes))]
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationSeriesType", Type = FieldType.Select, SelectOptions = typeof(SeriesTypes))]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Series series)
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Status";
|
||||
|
||||
[FieldDefinition(1, Label = "Status", Type = FieldType.Select, SelectOptions = typeof(SeriesStatusType))]
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationStatus", Type = FieldType.Select, SelectOptions = typeof(SeriesStatusType))]
|
||||
public int Status { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Series series)
|
||||
|
||||
@@ -23,10 +23,10 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Year";
|
||||
|
||||
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)]
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationMinimumYear", Type = FieldType.Number)]
|
||||
public int Min { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)]
|
||||
[FieldDefinition(2, Label = "AutoTaggingSpecificationMaximumYear", Type = FieldType.Number)]
|
||||
public int Max { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Series series)
|
||||
|
||||
@@ -6,6 +6,7 @@ using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||
@@ -276,6 +277,18 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("ChownGroup", value); }
|
||||
}
|
||||
|
||||
public ListSyncLevelType ListSyncLevel
|
||||
{
|
||||
get { return GetValueEnum("ListSyncLevel", ListSyncLevelType.Disabled); }
|
||||
set { SetValue("ListSyncLevel", value); }
|
||||
}
|
||||
|
||||
public int ListSyncTag
|
||||
{
|
||||
get { return GetValueInt("ListSyncTag"); }
|
||||
set { SetValue("ListSyncTag", value); }
|
||||
}
|
||||
|
||||
public int FirstDayOfWeek
|
||||
{
|
||||
get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -52,6 +53,9 @@ namespace NzbDrone.Core.Configuration
|
||||
int MaximumSize { get; set; }
|
||||
int MinimumAge { get; set; }
|
||||
|
||||
ListSyncLevelType ListSyncLevel { get; set; }
|
||||
int ListSyncTag { get; set; }
|
||||
|
||||
// UI
|
||||
int FirstDayOfWeek { get; set; }
|
||||
string CalendarWeekColumnHeader { get; set; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.History;
|
||||
@@ -23,10 +24,12 @@ namespace NzbDrone.Core.CustomFormats
|
||||
public class CustomFormatCalculationService : ICustomFormatCalculationService
|
||||
{
|
||||
private readonly ICustomFormatService _formatService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CustomFormatCalculationService(ICustomFormatService formatService)
|
||||
public CustomFormatCalculationService(ICustomFormatService formatService, Logger logger)
|
||||
{
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(RemoteEpisode remoteEpisode, long size)
|
||||
@@ -153,20 +156,23 @@ namespace NzbDrone.Core.CustomFormats
|
||||
return matches.OrderBy(x => x.Name).ToList();
|
||||
}
|
||||
|
||||
private static List<CustomFormat> ParseCustomFormat(EpisodeFile episodeFile, Series series, List<CustomFormat> allCustomFormats)
|
||||
private List<CustomFormat> ParseCustomFormat(EpisodeFile episodeFile, Series series, List<CustomFormat> allCustomFormats)
|
||||
{
|
||||
var releaseTitle = string.Empty;
|
||||
|
||||
if (episodeFile.SceneName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Trace("Using scene name for release title: {0}", episodeFile.SceneName);
|
||||
releaseTitle = episodeFile.SceneName;
|
||||
}
|
||||
else if (episodeFile.OriginalFilePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Trace("Using original file path for release title: {0}", Path.GetFileName(episodeFile.OriginalFilePath));
|
||||
releaseTitle = Path.GetFileName(episodeFile.OriginalFilePath);
|
||||
}
|
||||
else if (episodeFile.RelativePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Trace("Using relative path for release title: {0}", Path.GetFileName(episodeFile.RelativePath));
|
||||
releaseTitle = Path.GetFileName(episodeFile.RelativePath);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -27,9 +27,19 @@ namespace NzbDrone.Core.CustomFormats
|
||||
public override int Order => 3;
|
||||
public override string ImplementationName => "Language";
|
||||
|
||||
[FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))]
|
||||
[FieldDefinition(1, Label = "CustomFormatsSpecificationLanguage", 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.EpisodeInfo != null && input.Series != null && Value == Language.Original.Id && input.Series.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.EpisodeInfo != null && input.Series != null && Value == Language.Original.Id && input.Series.OriginalLanguage != Language.Unknown
|
||||
? input.Series.OriginalLanguage
|
||||
: (Language)Value;
|
||||
|
||||
return !input.Languages?.Contains(comparedLanguage) ?? false;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user