Compare commits
31 Commits
transmissi
...
develop
Author | SHA1 | Date |
---|---|---|
Bogdan | 2f04b037a1 | |
Bogdan | 7b87de2e93 | |
Bogdan | eb2fd13509 | |
Bogdan | ffdb08cfe6 | |
Mark McDowall | 37c4647f24 | |
Mark McDowall | f7a58aab33 | |
Mark McDowall | 4b186e894e | |
kephasdev | 35a2bc9403 | |
Bogdan | cc03ce04f1 | |
Bogdan | 363f8fc347 | |
RaZaSB | 0877a6718d | |
Bogdan | 8b253c36ea | |
Bogdan | e6f82270a9 | |
Mark McDowall | 813965e6a2 | |
Mark McDowall | 0d914f4c53 | |
Mark McDowall | ae7f73208a | |
Weblate | 4c86d673ea | |
Weblate | b1527f9abb | |
Bogdan | 291d792810 | |
Mark McDowall | 9b528eb829 | |
Mark McDowall | 4c0b896174 | |
Bogdan | 4ff83f9efc | |
Bogdan | 217611d716 | |
Mark McDowall | 1299a97579 | |
Mark McDowall | 4c0de55672 | |
Bogdan | 78a0def46a | |
Mark McDowall | 11a9dcb389 | |
Mark McDowall | 4eab168267 | |
Bogdan | c9b5a1258a | |
Mark McDowall | 9127a91dfc | |
Weblate | cc85a28ff7 |
|
@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||||
app_guid=${app_guid:-media}
|
app_guid=${app_guid:-media}
|
||||||
|
|
||||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||||
|
|
||||||
# Create User / Group as needed
|
# Create User / Group as needed
|
||||||
|
@ -114,7 +114,7 @@ case "$ARCH" in
|
||||||
esac
|
esac
|
||||||
echo ""
|
echo ""
|
||||||
echo "Removing previous tarballs"
|
echo "Removing previous tarballs"
|
||||||
# -f to Force so we fail if it doesnt exist
|
# -f to Force so we fail if it doesn't exist
|
||||||
rm -f "${app^}".*.tar.gz
|
rm -f "${app^}".*.tar.gz
|
||||||
echo ""
|
echo ""
|
||||||
echo "Downloading..."
|
echo "Downloading..."
|
||||||
|
|
|
@ -359,11 +359,16 @@ module.exports = {
|
||||||
],
|
],
|
||||||
|
|
||||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||||
'no-shadow': 'off',
|
'@typescript-eslint/no-unused-vars': [
|
||||||
// These should be enabled after cleaning things up
|
'error',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
{
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}
|
||||||
|
],
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'react/prop-types': 'off',
|
'no-shadow': 'off',
|
||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'error',
|
'error',
|
||||||
|
@ -376,7 +381,41 @@ module.exports = {
|
||||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
// React Hooks
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
|
||||||
|
// React
|
||||||
|
'react/function-component-definition': 'error',
|
||||||
|
'react/hook-use-state': 'error',
|
||||||
|
'react/jsx-boolean-value': ['error', 'always'],
|
||||||
|
'react/jsx-curly-brace-presence': [
|
||||||
|
'error',
|
||||||
|
{ props: 'never', children: 'never' }
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': 'error',
|
||||||
|
'react/jsx-handler-names': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
eventHandlerPrefix: 'on',
|
||||||
|
eventHandlerPropPrefix: 'on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||||
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||||
|
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||||
|
'react/jsx-sort-props': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
callbacksLast: true,
|
||||||
|
noSortAlphabetically: true,
|
||||||
|
reservedFirst: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -59,6 +59,7 @@ function Blocklist() {
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
page,
|
page,
|
||||||
|
pageSize,
|
||||||
totalPages,
|
totalPages,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
|
@ -223,6 +224,7 @@ function Blocklist() {
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
<TableOptionsModalWrapper
|
<TableOptionsModalWrapper
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
onTableOptionChange={handleTableOptionChange}
|
onTableOptionChange={handleTableOptionChange}
|
||||||
>
|
>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
@ -264,6 +266,7 @@ function Blocklist() {
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onTableOptionChange={handleTableOptionChange}
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
|
|
@ -53,6 +53,7 @@ function History() {
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
page,
|
page,
|
||||||
|
pageSize,
|
||||||
totalPages,
|
totalPages,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
} = useSelector((state: AppState) => state.history);
|
} = useSelector((state: AppState) => state.history);
|
||||||
|
@ -154,6 +155,7 @@ function History() {
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
<TableOptionsModalWrapper
|
<TableOptionsModalWrapper
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
onTableOptionChange={handleTableOptionChange}
|
onTableOptionChange={handleTableOptionChange}
|
||||||
>
|
>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
@ -193,6 +195,7 @@ function History() {
|
||||||
<div>
|
<div>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onTableOptionChange={handleTableOptionChange}
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
|
|
@ -73,6 +73,7 @@ function Queue() {
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
page,
|
page,
|
||||||
|
pageSize,
|
||||||
totalPages,
|
totalPages,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isGrabbing,
|
isGrabbing,
|
||||||
|
@ -269,8 +270,10 @@ function Queue() {
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
|
optionsComponent={QueueOptions}
|
||||||
onTableOptionChange={handleTableOptionChange}
|
onTableOptionChange={handleTableOptionChange}
|
||||||
onSelectAllChange={handleSelectAllChange}
|
onSelectAllChange={handleSelectAllChange}
|
||||||
onSortPress={handleSortPress}
|
onSortPress={handleSortPress}
|
||||||
|
@ -344,6 +347,7 @@ function Queue() {
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
<TableOptionsModalWrapper
|
<TableOptionsModalWrapper
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
maxPageSize={200}
|
maxPageSize={200}
|
||||||
optionsComponent={QueueOptions}
|
optionsComponent={QueueOptions}
|
||||||
onTableOptionChange={handleTableOptionChange}
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { Fragment, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
@ -22,24 +22,26 @@ function QueueOptions() {
|
||||||
[name]: value,
|
[name]: value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (name === 'includeUnknownSeriesItems') {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<FormGroup>
|
||||||
<FormGroup>
|
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
||||||
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="includeUnknownSeriesItems"
|
name="includeUnknownSeriesItems"
|
||||||
value={includeUnknownSeriesItems}
|
value={includeUnknownSeriesItems}
|
||||||
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
||||||
onChange={handleOptionChange}
|
onChange={handleOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,4 +26,5 @@
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 70px;
|
width: 70px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,10 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||||
|
|
||||||
|
function RedirectWithUrlBase() {
|
||||||
|
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||||
|
}
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -51,9 +55,7 @@ function AppRoutes() {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
addUrlBase={false}
|
addUrlBase={false}
|
||||||
render={() => {
|
render={RedirectWithUrlBase}
|
||||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -61,21 +63,9 @@ function AppRoutes() {
|
||||||
|
|
||||||
<Route path="/add/import" component={ImportSeries} />
|
<Route path="/add/import" component={ImportSeries} />
|
||||||
|
|
||||||
<Route
|
<Route path="/serieseditor" exact={true} render={RedirectWithUrlBase} />
|
||||||
path="/serieseditor"
|
|
||||||
exact={true}
|
|
||||||
render={() => {
|
|
||||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route path="/seasonpass" exact={true} render={RedirectWithUrlBase} />
|
||||||
path="/seasonpass"
|
|
||||||
exact={true}
|
|
||||||
render={() => {
|
|
||||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} />
|
<Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} />
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||||
<div>{info.componentStack}</div>
|
<div>{info.componentStack}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{<div className={styles.version}>Version: {window.Sonarr.version}</div>}
|
<div className={styles.version}>Version: {window.Sonarr.version}</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
@ -82,9 +82,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
<span>
|
<span>
|
||||||
{showSeasonNumber && seasonNumber != null && (
|
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
|
||||||
<Fragment>{seasonNumber}x</Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
||||||
|
|
||||||
|
@ -111,9 +109,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
{showSeasonNumber && seasonNumber != null && (
|
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
|
||||||
<Fragment>{seasonNumber}x</Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
|
||||||
export default function useModalOpenState(
|
export default function useModalOpenState(
|
||||||
initialState: boolean
|
initialState: boolean
|
||||||
): [boolean, () => void, () => void] {
|
): [boolean, () => void, () => void] {
|
||||||
const [isOpen, setOpen] = useState(initialState);
|
const [isOpen, setIsOpen] = useState(initialState);
|
||||||
|
|
||||||
const setModalOpen = useCallback(() => {
|
const setModalOpen = useCallback(() => {
|
||||||
setOpen(true);
|
setIsOpen(true);
|
||||||
}, [setOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
const setModalClosed = useCallback(() => {
|
const setModalClosed = useCallback(() => {
|
||||||
setOpen(false);
|
setIsOpen(false);
|
||||||
}, [setOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
return [isOpen, setModalOpen, setModalClosed];
|
return [isOpen, setModalOpen, setModalClosed];
|
||||||
}
|
}
|
||||||
|
|
|
@ -857,7 +857,7 @@ function InteractiveImportModalContent(
|
||||||
|
|
||||||
<MenuContent>
|
<MenuContent>
|
||||||
<SelectedMenuItem
|
<SelectedMenuItem
|
||||||
name={'all'}
|
name="all"
|
||||||
isSelected={!filterExistingFiles}
|
isSelected={!filterExistingFiles}
|
||||||
onPress={onFilterExistingFilesChange}
|
onPress={onFilterExistingFilesChange}
|
||||||
>
|
>
|
||||||
|
@ -865,7 +865,7 @@ function InteractiveImportModalContent(
|
||||||
</SelectedMenuItem>
|
</SelectedMenuItem>
|
||||||
|
|
||||||
<SelectedMenuItem
|
<SelectedMenuItem
|
||||||
name={'new'}
|
name="new"
|
||||||
isSelected={filterExistingFiles}
|
isSelected={filterExistingFiles}
|
||||||
onPress={onFilterExistingFilesChange}
|
onPress={onFilterExistingFilesChange}
|
||||||
>
|
>
|
||||||
|
@ -945,7 +945,7 @@ function InteractiveImportModalContent(
|
||||||
<SelectInput
|
<SelectInput
|
||||||
className={styles.bulkSelect}
|
className={styles.bulkSelect}
|
||||||
name="select"
|
name="select"
|
||||||
value={'select'}
|
value="select"
|
||||||
values={bulkSelectOptions}
|
values={bulkSelectOptions}
|
||||||
isDisabled={!selectedIds.length}
|
isDisabled={!selectedIds.length}
|
||||||
onChange={onSelectModalSelect}
|
onChange={onSelectModalSelect}
|
||||||
|
|
|
@ -17,7 +17,7 @@ function SelectLanguageModal(props: SelectLanguageModalProps) {
|
||||||
props;
|
props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||||
<SelectLanguageModalContent
|
<SelectLanguageModalContent
|
||||||
languageIds={languageIds}
|
languageIds={languageIds}
|
||||||
modalTitle={modalTitle}
|
modalTitle={modalTitle}
|
||||||
|
|
|
@ -64,19 +64,20 @@ interface RowItemData {
|
||||||
onSeriesSelect(seriesId: number): void;
|
onSeriesSelect(seriesId: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||||
index,
|
|
||||||
style,
|
|
||||||
data,
|
|
||||||
}) => {
|
|
||||||
const { items, columns, onSeriesSelect } = data;
|
const { items, columns, onSeriesSelect } = data;
|
||||||
|
const series = index >= items.length ? null : items[index];
|
||||||
|
|
||||||
if (index >= items.length) {
|
const handlePress = useCallback(() => {
|
||||||
|
if (series?.id) {
|
||||||
|
onSeriesSelect(series.id);
|
||||||
|
}
|
||||||
|
}, [series?.id, onSeriesSelect]);
|
||||||
|
|
||||||
|
if (series == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const series = items[index];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowButton
|
<VirtualTableRowButton
|
||||||
style={{
|
style={{
|
||||||
|
@ -84,7 +85,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
onPress={() => onSeriesSelect(series.id)}
|
onPress={handlePress}
|
||||||
>
|
>
|
||||||
<SelectSeriesRow
|
<SelectSeriesRow
|
||||||
key={series.id}
|
key={series.id}
|
||||||
|
@ -98,7 +99,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowButton>
|
</VirtualTableRowButton>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
|
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
|
||||||
const { modalTitle, onSeriesSelect, onModalClose } = props;
|
const { modalTitle, onSeriesSelect, onModalClose } = props;
|
||||||
|
@ -197,9 +198,9 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Scroller
|
<Scroller
|
||||||
|
ref={scrollerRef}
|
||||||
className={styles.scroller}
|
className={styles.scroller}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
ref={scrollerRef}
|
|
||||||
>
|
>
|
||||||
<SelectSeriesModalTableHeader columns={columns} />
|
<SelectSeriesModalTableHeader columns={columns} />
|
||||||
<List<RowItemData>
|
<List<RowItemData>
|
||||||
|
|
|
@ -17,7 +17,7 @@ function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
|
||||||
props;
|
props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||||
<SelectDownloadClientModalContent
|
<SelectDownloadClientModalContent
|
||||||
protocol={protocol}
|
protocol={protocol}
|
||||||
modalTitle={modalTitle}
|
modalTitle={modalTitle}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment, useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import ParseModal from 'Parse/ParseModal';
|
import ParseModal from 'Parse/ParseModal';
|
||||||
|
@ -16,7 +16,7 @@ function ParseToolbarButton() {
|
||||||
}, [setIsParseModalOpen]);
|
}, [setIsParseModalOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('TestParsing')}
|
label={translate('TestParsing')}
|
||||||
iconName={icons.PARSE}
|
iconName={icons.PARSE}
|
||||||
|
@ -24,7 +24,7 @@ function ParseToolbarButton() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
|
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ interface RootFolderRowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function RootFolderRow(props: RootFolderRowProps) {
|
function RootFolderRow(props: RootFolderRowProps) {
|
||||||
const { id, path, accessible, freeSpace, unmappedFolders = [] } = props;
|
const { id, path, accessible, freeSpace = 0, unmappedFolders = [] } = props;
|
||||||
|
|
||||||
const isUnavailable = !accessible;
|
const isUnavailable = !accessible;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
@ -6,14 +5,19 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './SeasonInfo.css';
|
import styles from './SeasonInfo.css';
|
||||||
|
|
||||||
function SeasonInfo(props) {
|
interface SeasonInfoProps {
|
||||||
const {
|
totalEpisodeCount: number;
|
||||||
totalEpisodeCount,
|
monitoredEpisodeCount: number;
|
||||||
monitoredEpisodeCount,
|
episodeFileCount: number;
|
||||||
episodeFileCount,
|
sizeOnDisk: number;
|
||||||
sizeOnDisk
|
}
|
||||||
} = props;
|
|
||||||
|
|
||||||
|
function SeasonInfo({
|
||||||
|
totalEpisodeCount,
|
||||||
|
monitoredEpisodeCount,
|
||||||
|
episodeFileCount,
|
||||||
|
sizeOnDisk,
|
||||||
|
}: SeasonInfoProps) {
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
@ -47,11 +51,4 @@ function SeasonInfo(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SeasonInfo.propTypes = {
|
|
||||||
totalEpisodeCount: PropTypes.number.isRequired,
|
|
||||||
monitoredEpisodeCount: PropTypes.number.isRequired,
|
|
||||||
episodeFileCount: PropTypes.number.isRequired,
|
|
||||||
sizeOnDisk: PropTypes.number.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SeasonInfo;
|
export default SeasonInfo;
|
|
@ -212,8 +212,8 @@ class SeriesDetails extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
episodeFileCount,
|
episodeFileCount = 0,
|
||||||
sizeOnDisk
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -454,10 +454,9 @@ class SeriesDetails extends Component {
|
||||||
name={icons.DRIVE}
|
name={icons.DRIVE}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.sizeOnDisk}>
|
<span className={styles.sizeOnDisk}>
|
||||||
{
|
{formatBytes(sizeOnDisk)}
|
||||||
formatBytes(sizeOnDisk || 0)
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
|
@ -194,10 +194,12 @@ function getInfoRowProps(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'sizeOnDisk') {
|
if (name === 'sizeOnDisk') {
|
||||||
|
const { sizeOnDisk = 0 } = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: translate('SizeOnDisk'),
|
title: translate('SizeOnDisk'),
|
||||||
iconName: icons.DRIVE,
|
iconName: icons.DRIVE,
|
||||||
label: formatBytes(props.sizeOnDisk),
|
label: formatBytes(sizeOnDisk),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,7 @@ interface SeriesIndexOverviewsProps {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||||
index,
|
|
||||||
style,
|
|
||||||
data,
|
|
||||||
}) => {
|
|
||||||
const { items, ...otherData } = data;
|
const { items, ...otherData } = data;
|
||||||
|
|
||||||
if (index >= items.length) {
|
if (index >= items.length) {
|
||||||
|
@ -60,7 +56,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||||
<SeriesIndexOverview seriesId={series.id} {...otherData} />
|
<SeriesIndexOverview seriesId={series.id} {...otherData} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function getWindowScrollTopPosition() {
|
function getWindowScrollTopPosition() {
|
||||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
|
|
@ -37,7 +37,7 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
|
||||||
added,
|
added,
|
||||||
seasonCount,
|
seasonCount,
|
||||||
path,
|
path,
|
||||||
sizeOnDisk,
|
sizeOnDisk = 0,
|
||||||
tags,
|
tags,
|
||||||
sortKey,
|
sortKey,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
|
|
|
@ -60,12 +60,12 @@ const seriesIndexSelector = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
|
function Cell({
|
||||||
columnIndex,
|
columnIndex,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
style,
|
style,
|
||||||
data,
|
data,
|
||||||
}) => {
|
}: GridChildComponentProps<CellItemData>) {
|
||||||
const { layout, items, sortKey, isSelectMode } = data;
|
const { layout, items, sortKey, isSelectMode } = data;
|
||||||
const { columnCount, padding, posterWidth, posterHeight } = layout;
|
const { columnCount, padding, posterWidth, posterHeight } = layout;
|
||||||
const index = rowIndex * columnCount + columnIndex;
|
const index = rowIndex * columnCount + columnIndex;
|
||||||
|
@ -92,7 +92,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function getWindowScrollTopPosition() {
|
function getWindowScrollTopPosition() {
|
||||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
|
|
@ -45,11 +45,7 @@ const columnsSelector = createSelector(
|
||||||
(columns) => columns
|
(columns) => columns
|
||||||
);
|
);
|
||||||
|
|
||||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||||
index,
|
|
||||||
style,
|
|
||||||
data,
|
|
||||||
}) => {
|
|
||||||
const { items, sortKey, columns, isSelectMode } = data;
|
const { items, sortKey, columns, isSelectMode } = data;
|
||||||
|
|
||||||
if (index >= items.length) {
|
if (index >= items.length) {
|
||||||
|
@ -75,7 +71,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function getWindowScrollTopPosition() {
|
function getWindowScrollTopPosition() {
|
||||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
@ -32,7 +32,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('ShowBanners')}</FormLabel>
|
<FormLabel>{translate('ShowBanners')}</FormLabel>
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
|
||||||
onChange={onTableOptionChangeWrapper}
|
onChange={onTableOptionChangeWrapper}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
@ -17,11 +17,11 @@ function CustomFormatSettingsPage() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
showSave={false}
|
showSave={false}
|
||||||
additionalButtons={
|
additionalButtons={
|
||||||
<Fragment>
|
<>
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<ParseToolbarButton />
|
<ParseToolbarButton />
|
||||||
</Fragment>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -231,9 +231,9 @@ function ManageDownloadClientsModalContent(
|
||||||
selectAll={true}
|
selectAll={true}
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
onSortPress={onSortPress}
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
|
||||||
|
|
||||||
<ManageDownloadClientsEditModal
|
<ManageDownloadClientsEditModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
|
downloadClientIds={selectedIds}
|
||||||
onModalClose={onEditModalClose}
|
onModalClose={onEditModalClose}
|
||||||
onSavePress={onSavePress}
|
onSavePress={onSavePress}
|
||||||
downloadClientIds={selectedIds}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TagsModal
|
<TagsModal
|
||||||
|
|
|
@ -32,7 +32,7 @@ function EditImportListExclusionModal(
|
||||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
|
||||||
<EditImportListExclusionModalContent
|
<EditImportListExclusionModalContent
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClosePress}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -261,9 +261,9 @@ function ManageImportListsModalContent(
|
||||||
|
|
||||||
<ManageImportListsEditModal
|
<ManageImportListsEditModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
|
importListIds={selectedIds}
|
||||||
onModalClose={onEditModalClose}
|
onModalClose={onEditModalClose}
|
||||||
onSavePress={onSavePress}
|
onSavePress={onSavePress}
|
||||||
importListIds={selectedIds}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TagsModal
|
<TagsModal
|
||||||
|
|
|
@ -226,9 +226,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
selectAll={true}
|
selectAll={true}
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
onSortPress={onSortPress}
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
|
||||||
<ManageIndexersEditModal
|
<ManageIndexersEditModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
|
indexerIds={selectedIds}
|
||||||
onModalClose={onEditModalClose}
|
onModalClose={onEditModalClose}
|
||||||
onSavePress={onSavePress}
|
onSavePress={onSavePress}
|
||||||
indexerIds={selectedIds}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TagsModal
|
<TagsModal
|
||||||
|
|
|
@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
...queryParams
|
queryParams
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
dispatch(set({ section, isDeleting: true }));
|
dispatch(set({ section, isDeleting: true }));
|
||||||
|
|
|
@ -251,6 +251,11 @@ export const filterBuilderProps = [
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.EXACT,
|
||||||
valueType: filterBuilderValueTypes.SERIES_TYPES
|
valueType: filterBuilderValueTypes.SERIES_TYPES
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: () => translate('Title'),
|
||||||
|
type: filterBuilderTypes.STRING
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'network',
|
name: 'network',
|
||||||
label: () => translate('Network'),
|
label: () => translate('Network'),
|
||||||
|
|
|
@ -66,7 +66,7 @@ function About() {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isDocker ? (
|
{isDocker ? (
|
||||||
<DescriptionListItem title={translate('Docker')} data={'Yes'} />
|
<DescriptionListItem title={translate('Docker')} data="Yes" />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
Fragment,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
|
@ -158,7 +152,7 @@ function Updates() {
|
||||||
{translate('InstallLatest')}
|
{translate('InstallLatest')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<>
|
||||||
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
||||||
|
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
|
@ -171,7 +165,7 @@ function Updates() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { filesize } from 'filesize';
|
import { filesize } from 'filesize';
|
||||||
|
|
||||||
function formatBytes(input?: string | number) {
|
function formatBytes(input: string | number) {
|
||||||
if (!input) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = Number(input);
|
const size = Number(input);
|
||||||
|
|
||||||
if (isNaN(size)) {
|
if (isNaN(size)) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Test.ExtensionTests
|
namespace NzbDrone.Common.Test.ExtensionTests
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class Int64ExtensionFixture
|
public class NumberExtensionFixture
|
||||||
{
|
{
|
||||||
[TestCase(0, "0 B")]
|
[TestCase(0, "0 B")]
|
||||||
[TestCase(1000, "1,000.0 B")]
|
[TestCase(1000, "1,000.0 B")]
|
|
@ -380,8 +380,17 @@ namespace NzbDrone.Common.Test
|
||||||
[TestCase(@" C:\Test\TV\")]
|
[TestCase(@" C:\Test\TV\")]
|
||||||
[TestCase(@" C:\Test\TV")]
|
[TestCase(@" C:\Test\TV")]
|
||||||
|
|
||||||
public void IsPathValid_should_be_false(string path)
|
public void IsPathValid_should_be_false_on_windows(string path)
|
||||||
{
|
{
|
||||||
|
WindowsOnly();
|
||||||
|
path.IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(@"")]
|
||||||
|
[TestCase(@"relative/path")]
|
||||||
|
public void IsPathValid_should_be_false_on_unix(string path)
|
||||||
|
{
|
||||||
|
PosixOnly();
|
||||||
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Extensions
|
namespace NzbDrone.Common.Extensions
|
||||||
{
|
{
|
||||||
public static class Int64Extensions
|
public static class NumberExtensions
|
||||||
{
|
{
|
||||||
private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
|
private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
|
||||||
|
|
||||||
|
@ -26,5 +26,25 @@ namespace NzbDrone.Common.Extensions
|
||||||
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, "{0:n1} {1}", adjustedSize, SizeSuffixes[mag]);
|
return string.Format(CultureInfo.InvariantCulture, "{0:n1} {1}", adjustedSize, SizeSuffixes[mag]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long Megabytes(this int megabytes)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(megabytes * 1024L * 1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long Gigabytes(this int gigabytes)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long Megabytes(this double megabytes)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(megabytes * 1024L * 1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long Gigabytes(this double gigabytes)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -152,16 +152,20 @@ namespace NzbDrone.Common.Extensions
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var directoryInfo = new DirectoryInfo(path);
|
// Only check for leading or trailing spaces for path when running on Windows.
|
||||||
|
if (OsInfo.IsWindows)
|
||||||
while (directoryInfo != null)
|
|
||||||
{
|
{
|
||||||
if (directoryInfo.Name.Trim() != directoryInfo.Name)
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
directoryInfo = directoryInfo.Parent;
|
while (directoryInfo != null)
|
||||||
|
{
|
||||||
|
if (directoryInfo.Name.Trim() != directoryInfo.Name)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryInfo = directoryInfo.Parent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationType == PathValidationType.AnyOs)
|
if (validationType == PathValidationType.AnyOs)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Fluent;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Extensions
|
namespace NzbDrone.Common.Instrumentation.Extensions
|
||||||
{
|
{
|
||||||
|
@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
||||||
{
|
{
|
||||||
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
||||||
|
|
||||||
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
|
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||||
{
|
{
|
||||||
return logBuilder.Property("Sentry", fingerprint);
|
return logBuilder.Property("Sentry", fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
|
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||||
{
|
{
|
||||||
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
|
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
|
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||||
{
|
{
|
||||||
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
|
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
|
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||||
{
|
{
|
||||||
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
|
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
|
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||||
{
|
{
|
||||||
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
|
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
|
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||||
{
|
{
|
||||||
SentryLogger.Log(level)
|
SentryLogger.ForLogEvent(level)
|
||||||
.CopyLogEvent(logBuilder.LogEventInfo)
|
.CopyLogEvent(logBuilder.LogEvent)
|
||||||
.SentryFingerprint(fingerprint)
|
.SentryFingerprint(fingerprint)
|
||||||
.Write();
|
.Log();
|
||||||
|
|
||||||
return logBuilder.Property("Sentry", null);
|
return logBuilder.Property<string>("Sentry", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
|
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
|
||||||
{
|
{
|
||||||
return logBuilder.LoggerName(logEvent.LoggerName)
|
return logBuilder.TimeStamp(logEvent.TimeStamp)
|
||||||
.TimeStamp(logEvent.TimeStamp)
|
|
||||||
.Message(logEvent.Message, logEvent.Parameters)
|
.Message(logEvent.Message, logEvent.Parameters)
|
||||||
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
|
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
|
||||||
.Exception(logEvent.Exception);
|
.Exception(logEvent.Exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
using NLog;
|
using System.Text;
|
||||||
|
using NLog;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation
|
namespace NzbDrone.Common.Instrumentation
|
||||||
{
|
{
|
||||||
public class NzbDroneFileTarget : FileTarget
|
public class NzbDroneFileTarget : FileTarget
|
||||||
{
|
{
|
||||||
protected override string GetFormattedMessage(LogEventInfo logEvent)
|
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||||
{
|
{
|
||||||
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||||
|
target.Append(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Config;
|
using NLog.Config;
|
||||||
|
using NLog.Layouts.ClefJsonLayout;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
@ -13,6 +14,8 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
public static class NzbDroneLogger
|
public static class NzbDroneLogger
|
||||||
{
|
{
|
||||||
private const string FILE_LOG_LAYOUT = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}";
|
private const string FILE_LOG_LAYOUT = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}";
|
||||||
|
public const string ConsoleLogLayout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}";
|
||||||
|
public static CompactJsonLayout ClefLogLayout = new CompactJsonLayout();
|
||||||
|
|
||||||
private static bool _isConfigured;
|
private static bool _isConfigured;
|
||||||
|
|
||||||
|
@ -34,6 +37,8 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
|
|
||||||
var appFolderInfo = new AppFolderInfo(startupContext);
|
var appFolderInfo = new AppFolderInfo(startupContext);
|
||||||
|
|
||||||
|
RegisterGlobalFilters();
|
||||||
|
|
||||||
if (Debugger.IsAttached)
|
if (Debugger.IsAttached)
|
||||||
{
|
{
|
||||||
RegisterDebugger();
|
RegisterDebugger();
|
||||||
|
@ -122,7 +127,16 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
var coloredConsoleTarget = new ColoredConsoleTarget();
|
var coloredConsoleTarget = new ColoredConsoleTarget();
|
||||||
|
|
||||||
coloredConsoleTarget.Name = "consoleLogger";
|
coloredConsoleTarget.Name = "consoleLogger";
|
||||||
coloredConsoleTarget.Layout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}}";
|
|
||||||
|
var logFormat = Enum.TryParse<ConsoleLogFormat>(Environment.GetEnvironmentVariable("SONARR__LOG__CONSOLEFORMAT"), out var formatEnumValue)
|
||||||
|
? formatEnumValue
|
||||||
|
: ConsoleLogFormat.Standard;
|
||||||
|
|
||||||
|
coloredConsoleTarget.Layout = logFormat switch
|
||||||
|
{
|
||||||
|
ConsoleLogFormat.Clef => ClefLogLayout,
|
||||||
|
_ => ConsoleLogLayout
|
||||||
|
};
|
||||||
|
|
||||||
var loggingRule = new LoggingRule("*", level, coloredConsoleTarget);
|
var loggingRule = new LoggingRule("*", level, coloredConsoleTarget);
|
||||||
|
|
||||||
|
@ -148,7 +162,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
fileTarget.ConcurrentWrites = false;
|
fileTarget.ConcurrentWrites = false;
|
||||||
fileTarget.ConcurrentWriteAttemptDelay = 50;
|
fileTarget.ConcurrentWriteAttemptDelay = 50;
|
||||||
fileTarget.ConcurrentWriteAttempts = 10;
|
fileTarget.ConcurrentWriteAttempts = 10;
|
||||||
fileTarget.ArchiveAboveSize = 1024000;
|
fileTarget.ArchiveAboveSize = 1.Megabytes();
|
||||||
fileTarget.MaxArchiveFiles = maxArchiveFiles;
|
fileTarget.MaxArchiveFiles = maxArchiveFiles;
|
||||||
fileTarget.EnableFileDelete = true;
|
fileTarget.EnableFileDelete = true;
|
||||||
fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling;
|
fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling;
|
||||||
|
@ -196,6 +210,17 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
LogManager.Configuration.LoggingRules.Insert(0, rule);
|
LogManager.Configuration.LoggingRules.Insert(0, rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void RegisterGlobalFilters()
|
||||||
|
{
|
||||||
|
LogManager.Setup().LoadConfiguration(c =>
|
||||||
|
{
|
||||||
|
c.ForLogger("System.*").WriteToNil(LogLevel.Warn);
|
||||||
|
c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn);
|
||||||
|
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
|
||||||
|
c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static Logger GetLogger(Type obj)
|
public static Logger GetLogger(Type obj)
|
||||||
{
|
{
|
||||||
return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", ""));
|
return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", ""));
|
||||||
|
@ -206,4 +231,10 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
return GetLogger(obj.GetType());
|
return GetLogger(obj.GetType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ConsoleLogFormat
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
Clef
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ public class LogOptions
|
||||||
public string Level { get; set; }
|
public string Level { get; set; }
|
||||||
public bool? FilterSentryEvents { get; set; }
|
public bool? FilterSentryEvents { get; set; }
|
||||||
public int? Rotate { get; set; }
|
public int? Rotate { get; set; }
|
||||||
|
public int? SizeLimit { get; set; }
|
||||||
public bool? Sql { get; set; }
|
public bool? Sql { get; set; }
|
||||||
public string ConsoleLevel { get; set; }
|
public string ConsoleLevel { get; set; }
|
||||||
|
public string ConsoleFormat { get; set; }
|
||||||
public bool? AnalyticsEnabled { get; set; }
|
public bool? AnalyticsEnabled { get; set; }
|
||||||
public string SyslogServer { get; set; }
|
public string SyslogServer { get; set; }
|
||||||
public int? SyslogPort { get; set; }
|
public int? SyslogPort { get; set; }
|
||||||
|
|
|
@ -8,9 +8,10 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="4.7.14" />
|
<PackageReference Include="NLog" Version="5.3.2" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" />
|
||||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Datastore.Migration;
|
using NzbDrone.Core.Datastore.Migration;
|
||||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||||
|
|
|
@ -4,6 +4,7 @@ using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
|
|
@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
.Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||||
.Returns(_remoteEpisode);
|
.Returns(_remoteEpisode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Subject.GetRssDecision(_reports).ToList();
|
Subject.GetRssDecision(_reports).ToList();
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||||
|
|
||||||
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||||
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||||
|
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
var results = Subject.GetRssDecision(_reports).ToList();
|
var results = Subject.GetRssDecision(_reports).ToList();
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||||
|
|
||||||
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||||
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||||
|
@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Subject.GetSearchDecision(_reports, new SingleEpisodeSearchCriteria()).ToList();
|
Subject.GetSearchDecision(_reports, new SingleEpisodeSearchCriteria()).ToList();
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Never());
|
||||||
|
|
||||||
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||||
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
|
||||||
|
@ -212,7 +212,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
{
|
{
|
||||||
GivenSpecifications(_pass1);
|
GivenSpecifications(_pass1);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||||
.Throws<TestException>();
|
.Throws<TestException>();
|
||||||
|
|
||||||
_reports = new List<ReleaseInfo>
|
_reports = new List<ReleaseInfo>
|
||||||
|
@ -224,7 +224,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Subject.GetRssDecision(_reports);
|
Subject.GetRssDecision(_reports);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()), Times.Exactly(_reports.Count));
|
Mocker.GetMock<IParsingService>().Verify(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()), Times.Exactly(_reports.Count));
|
||||||
|
|
||||||
ExceptionVerification.ExpectedErrors(3);
|
ExceptionVerification.ExpectedErrors(3);
|
||||||
}
|
}
|
||||||
|
@ -263,8 +263,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
.Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||||
.Returns<ParsedEpisodeInfo, int, int, SearchCriteriaBase>((p, tvdbid, tvrageid, c) =>
|
.Returns<ParsedEpisodeInfo, int, int, string, SearchCriteriaBase>((p, _, _, _, _) =>
|
||||||
new RemoteEpisode
|
new RemoteEpisode
|
||||||
{
|
{
|
||||||
DownloadAllowed = true,
|
DownloadAllowed = true,
|
||||||
|
@ -318,7 +318,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
{
|
{
|
||||||
GivenSpecifications(_pass1);
|
GivenSpecifications(_pass1);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
Mocker.GetMock<IParsingService>().Setup(c => c.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
|
||||||
.Throws<TestException>();
|
.Throws<TestException>();
|
||||||
|
|
||||||
_reports = new List<ReleaseInfo>
|
_reports = new List<ReleaseInfo>
|
||||||
|
|
|
@ -4,6 +4,8 @@ using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Download.Aggregation.Aggregators;
|
using NzbDrone.Core.Download.Aggregation.Aggregators;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.Indexers.TorrentRss;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
@ -62,6 +64,63 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
||||||
Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(_remoteEpisode.ParsedEpisodeInfo.Languages);
|
Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(_remoteEpisode.ParsedEpisodeInfo.Languages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_multi_languages_when_indexer_has_multi_languages_configuration()
|
||||||
|
{
|
||||||
|
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||||
|
var indexerDefinition = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
|
};
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.Get(1))
|
||||||
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
|
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle);
|
||||||
|
_remoteEpisode.Release.IndexerId = 1;
|
||||||
|
_remoteEpisode.Release.Title = releaseTitle;
|
||||||
|
|
||||||
|
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_multi_languages_when_release_as_unknown_as_default_language_and_indexer_has_multi_languages_configuration()
|
||||||
|
{
|
||||||
|
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||||
|
var indexerDefinition = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
|
};
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.Get(1))
|
||||||
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
|
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { Language.Unknown }, releaseTitle);
|
||||||
|
_remoteEpisode.Release.IndexerId = 1;
|
||||||
|
_remoteEpisode.Release.Title = releaseTitle;
|
||||||
|
|
||||||
|
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_original_when_indexer_has_no_multi_languages_configuration()
|
||||||
|
{
|
||||||
|
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||||
|
var indexerDefinition = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Settings = new TorrentRssIndexerSettings { }
|
||||||
|
};
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.Get(1))
|
||||||
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
|
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle);
|
||||||
|
_remoteEpisode.Release.IndexerId = 1;
|
||||||
|
_remoteEpisode.Release.Title = releaseTitle;
|
||||||
|
|
||||||
|
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage });
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title()
|
public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title()
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,6 @@ using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
|
||||||
using NzbDrone.Core.Localization;
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
@ -35,7 +34,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
|
||||||
.Returns(30);
|
.Returns(30);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), (SearchCriteriaBase)null))
|
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(() => CreateRemoteEpisode());
|
.Returns(() => CreateRemoteEpisode());
|
||||||
|
|
||||||
Mocker.GetMock<IHttpClient>()
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
|
|
@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||||
.Returns(remoteEpisode);
|
.Returns(remoteEpisode);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
.Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(remoteEpisode.ParsedEpisodeInfo);
|
.Returns(remoteEpisode.ParsedEpisodeInfo);
|
||||||
|
|
||||||
var client = new DownloadClientDefinition()
|
var client = new DownloadClientDefinition()
|
||||||
|
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(remoteEpisode);
|
.Returns(remoteEpisode);
|
||||||
|
|
||||||
Mocker.GetMock<IHistoryService>()
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
@ -199,7 +199,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(default(RemoteEpisode));
|
.Returns(default(RemoteEpisode));
|
||||||
|
|
||||||
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
|
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
|
||||||
|
@ -228,7 +228,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(default(RemoteEpisode));
|
.Returns(default(RemoteEpisode));
|
||||||
|
|
||||||
Mocker.GetMock<IHistoryService>()
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
@ -258,7 +258,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(default(RemoteEpisode));
|
.Returns(default(RemoteEpisode));
|
||||||
|
|
||||||
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
|
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
|
||||||
|
@ -287,7 +287,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(default(RemoteEpisode));
|
.Returns(default(RemoteEpisode));
|
||||||
|
|
||||||
Mocker.GetMock<IHistoryService>()
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
@ -317,7 +317,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
|
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||||
.Returns(default(RemoteEpisode));
|
.Returns(default(RemoteEpisode));
|
||||||
|
|
||||||
Subject.Handle(new SeriesDeletedEvent(new List<Series> { remoteEpisode.Series }, true, true));
|
Subject.Handle(new SeriesDeletedEvent(new List<Series> { remoteEpisode.Series }, true, true));
|
||||||
|
|
|
@ -15,6 +15,10 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||||
[TestCase("Franklin & Bash", "Franklin+and+Bash")]
|
[TestCase("Franklin & Bash", "Franklin+and+Bash")]
|
||||||
[TestCase("Chicago P.D.", "Chicago+PD")]
|
[TestCase("Chicago P.D.", "Chicago+PD")]
|
||||||
[TestCase("Kourtney And Khlo\u00E9 Take The Hamptons", "Kourtney+And+Khloe+Take+The+Hamptons")]
|
[TestCase("Kourtney And Khlo\u00E9 Take The Hamptons", "Kourtney+And+Khloe+Take+The+Hamptons")]
|
||||||
|
[TestCase("Betty White`s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||||
|
[TestCase("Betty White\u00b4s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||||
|
[TestCase("Betty White‘s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||||
|
[TestCase("Betty White’s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||||
public void should_replace_some_special_characters(string input, string expected)
|
public void should_replace_some_special_characters(string input, string expected)
|
||||||
{
|
{
|
||||||
Subject.SceneTitles = new List<string> { input };
|
Subject.SceneTitles = new List<string> { input };
|
||||||
|
|
|
@ -7,6 +7,7 @@ using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
|
|
@ -6,6 +6,7 @@ using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.History;
|
using NzbDrone.Core.History;
|
||||||
|
@ -74,8 +75,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||||
.Returns(new List<EpisodeHistory>());
|
.Returns(new List<EpisodeHistory>());
|
||||||
|
|
||||||
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
|
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
|
||||||
.With(d => d.OutputPath = new OsPath(outputPath))
|
.With(d => d.OutputPath = new OsPath(outputPath))
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GivenNewDownload()
|
private void GivenNewDownload()
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
|
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
|
@ -233,11 +233,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
||||||
GivenEpisodes(actualInfo, actualInfo.EpisodeNumbers);
|
GivenEpisodes(actualInfo, actualInfo.EpisodeNumbers);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(v => v.ParseSpecialEpisodeTitle(fileInfo, It.IsAny<string>(), 0, 0, null))
|
.Setup(v => v.ParseSpecialEpisodeTitle(fileInfo, It.IsAny<string>(), 0, 0, null, null))
|
||||||
.Returns(actualInfo);
|
.Returns(actualInfo);
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Setup(v => v.ParseSpecialEpisodeTitle(folderInfo, It.IsAny<string>(), 0, 0, null))
|
.Setup(v => v.ParseSpecialEpisodeTitle(folderInfo, It.IsAny<string>(), 0, 0, null, null))
|
||||||
.Returns(actualInfo);
|
.Returns(actualInfo);
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(localEpisode, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(localEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
|
|
@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
GivenDailySeries();
|
GivenDailySeries();
|
||||||
GivenDailyParseResult();
|
GivenDailyParseResult();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
|
||||||
|
@ -103,7 +103,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
GivenDailySeries();
|
GivenDailySeries();
|
||||||
GivenDailyParseResult();
|
GivenDailyParseResult();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
|
||||||
|
@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
GivenDailySeries();
|
GivenDailySeries();
|
||||||
_parsedEpisodeInfo.AirDate = DateTime.Today.AddDays(-5).ToString(Episode.AIR_DATE_FORMAT);
|
_parsedEpisodeInfo.AirDate = DateTime.Today.AddDays(-5).ToString(Episode.AIR_DATE_FORMAT);
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
|
||||||
|
@ -128,7 +128,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
GivenDailyParseResult();
|
GivenDailyParseResult();
|
||||||
_parsedEpisodeInfo.DailyPart = 1;
|
_parsedEpisodeInfo.DailyPart = 1;
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), 1), Times.Once());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), 1), Times.Once());
|
||||||
|
@ -143,7 +143,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
.Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>()))
|
.Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>()))
|
||||||
.Returns(new List<Episode>());
|
.Returns(new List<Episode>());
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
|
||||||
|
@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenSceneNumberingSeries();
|
GivenSceneNumberingSeries();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||||
|
@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenSceneNumberingSeries();
|
GivenSceneNumberingSeries();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
||||||
|
@ -177,7 +177,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
GivenSceneNumberingSeries();
|
GivenSceneNumberingSeries();
|
||||||
_episodes.First().SceneEpisodeNumber = 10;
|
_episodes.First().SceneEpisodeNumber = 10;
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||||
|
@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_find_episode()
|
public void should_find_episode()
|
||||||
{
|
{
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||||
|
@ -195,7 +195,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_match_episode_with_search_criteria()
|
public void should_match_episode_with_search_criteria()
|
||||||
{
|
{
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
||||||
|
@ -206,7 +206,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
_episodes.First().EpisodeNumber = 10;
|
_episodes.First().EpisodeNumber = 10;
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once());
|
||||||
|
@ -537,7 +537,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
.With(e => e.EpisodeNumber = 1)
|
.With(e => e.EpisodeNumber = 1)
|
||||||
.Build());
|
.Build());
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(_series.TvdbId, 0, 1), Times.Once());
|
.Verify(v => v.FindEpisode(_series.TvdbId, 0, 1), Times.Once());
|
||||||
|
@ -555,7 +555,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
.Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle))
|
.Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle))
|
||||||
.Returns((Episode)null);
|
.Returns((Episode)null);
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.FindEpisode(_series.TvdbId, _parsedEpisodeInfo.SeasonNumber, _parsedEpisodeInfo.EpisodeNumbers.First()), Times.Once());
|
.Verify(v => v.FindEpisode(_series.TvdbId, _parsedEpisodeInfo.SeasonNumber, _parsedEpisodeInfo.EpisodeNumbers.First()), Times.Once());
|
||||||
|
|
|
@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenMatchBySeriesTitle();
|
GivenMatchBySeriesTitle();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once());
|
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once());
|
||||||
|
@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenMatchByTvdbId();
|
GivenMatchByTvdbId();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
||||||
|
@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenMatchByTvRageId();
|
GivenMatchByTvRageId();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId);
|
Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId, null);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once());
|
||||||
|
@ -123,7 +123,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
.Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
|
.Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||||
.Returns(new SceneMapping { TvdbId = 10 });
|
.Returns(new SceneMapping { TvdbId = 10 });
|
||||||
|
|
||||||
var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never());
|
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never());
|
||||||
|
@ -136,7 +136,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenMatchBySeriesTitle();
|
GivenMatchBySeriesTitle();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
||||||
|
@ -147,7 +147,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once());
|
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once());
|
||||||
|
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
.Setup(s => s.FindByTitle(_parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, _parsedEpisodeInfo.SeriesTitleInfo.Year))
|
.Setup(s => s.FindByTitle(_parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, _parsedEpisodeInfo.SeriesTitleInfo.Year))
|
||||||
.Returns(_series);
|
.Returns(_series);
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>()), Times.Once());
|
||||||
|
@ -180,7 +180,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
||||||
|
@ -191,7 +191,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, 0, 10, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, 0, 10, null, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once());
|
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once());
|
||||||
|
@ -202,12 +202,34 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never());
|
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_FindByImdbId_when_search_criteria_and_FindByTitle_matching_fails()
|
||||||
|
{
|
||||||
|
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||||
|
|
||||||
|
Subject.Map(_parsedEpisodeInfo, 0, 0, "tt12345", _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
|
Mocker.GetMock<ISeriesService>()
|
||||||
|
.Verify(v => v.FindByImdbId(It.IsAny<string>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_FindByImdbId_when_search_criteria_and_FindByTitle_matching_fails_and_tvdb_id_is_specified()
|
||||||
|
{
|
||||||
|
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||||
|
|
||||||
|
Subject.Map(_parsedEpisodeInfo, 10, 10, "tt12345", _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
|
Mocker.GetMock<ISeriesService>()
|
||||||
|
.Verify(v => v.FindByImdbId(It.IsAny<string>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_use_tvdbid_matching_when_alias_is_found()
|
public void should_use_tvdbid_matching_when_alias_is_found()
|
||||||
{
|
{
|
||||||
|
@ -215,7 +237,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
.Setup(s => s.FindTvdbId(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
|
.Setup(s => s.FindTvdbId(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||||
.Returns(_series.TvdbId);
|
.Returns(_series.TvdbId);
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
||||||
|
@ -226,7 +248,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||||
{
|
{
|
||||||
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
GivenParseResultSeriesDoesntMatchSearchCriteria();
|
||||||
|
|
||||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
|
||||||
|
|
||||||
Mocker.GetMock<ISeriesService>()
|
Mocker.GetMock<ISeriesService>()
|
||||||
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
|
||||||
|
|
|
@ -185,8 +185,10 @@ namespace NzbDrone.Core.Blocklisting
|
||||||
Indexer = message.Data.GetValueOrDefault("indexer"),
|
Indexer = message.Data.GetValueOrDefault("indexer"),
|
||||||
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
|
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
|
||||||
Message = message.Message,
|
Message = message.Message,
|
||||||
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"),
|
Languages = message.Languages,
|
||||||
Languages = message.Languages
|
TorrentInfoHash = message.TrackedDownload?.Protocol == DownloadProtocol.Torrent
|
||||||
|
? message.TrackedDownload.DownloadItem.DownloadId
|
||||||
|
: message.Data.GetValueOrDefault("torrentInfoHash", null)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
|
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
|
||||||
|
|
|
@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Instrumentation;
|
||||||
using NzbDrone.Common.Options;
|
using NzbDrone.Common.Options;
|
||||||
using NzbDrone.Core.Authentication;
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration.Events;
|
using NzbDrone.Core.Configuration.Events;
|
||||||
|
@ -38,8 +39,10 @@ namespace NzbDrone.Core.Configuration
|
||||||
bool AnalyticsEnabled { get; }
|
bool AnalyticsEnabled { get; }
|
||||||
string LogLevel { get; }
|
string LogLevel { get; }
|
||||||
string ConsoleLogLevel { get; }
|
string ConsoleLogLevel { get; }
|
||||||
|
ConsoleLogFormat ConsoleLogFormat { get; }
|
||||||
bool LogSql { get; }
|
bool LogSql { get; }
|
||||||
int LogRotate { get; }
|
int LogRotate { get; }
|
||||||
|
int LogSizeLimit { get; }
|
||||||
bool FilterSentryEvents { get; }
|
bool FilterSentryEvents { get; }
|
||||||
string Branch { get; }
|
string Branch { get; }
|
||||||
string ApiKey { get; }
|
string ApiKey { get; }
|
||||||
|
@ -220,9 +223,14 @@ namespace NzbDrone.Core.Configuration
|
||||||
|
|
||||||
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant();
|
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant();
|
||||||
|
|
||||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
|
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant();
|
||||||
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||||
|
|
||||||
|
public ConsoleLogFormat ConsoleLogFormat =>
|
||||||
|
Enum.TryParse<ConsoleLogFormat>(_logOptions.ConsoleFormat, out var enumValue)
|
||||||
|
? enumValue
|
||||||
|
: GetValueEnum("ConsoleLogFormat", ConsoleLogFormat.Standard, false);
|
||||||
|
|
||||||
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
|
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
|
||||||
|
|
||||||
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
|
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
|
||||||
|
@ -234,6 +242,7 @@ namespace NzbDrone.Core.Configuration
|
||||||
public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false);
|
public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false);
|
||||||
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
|
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
|
||||||
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
|
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
|
||||||
|
public int LogSizeLimit => Math.Min(Math.Max(_logOptions.SizeLimit ?? GetValueInt("LogSizeLimit", 1, persist: false), 0), 10);
|
||||||
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||||
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
||||||
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,9 @@ namespace NzbDrone.Core.DataAugmentation.Scene
|
||||||
sceneSeasonNumbers.Contains(n.SceneSeasonNumber ?? -1) ||
|
sceneSeasonNumbers.Contains(n.SceneSeasonNumber ?? -1) ||
|
||||||
((n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1 && n.SceneOrigin != "tvdb"))
|
((n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1 && n.SceneOrigin != "tvdb"))
|
||||||
.Where(n => IsEnglish(n.SearchTerm))
|
.Where(n => IsEnglish(n.SearchTerm))
|
||||||
.Select(n => n.SearchTerm).Distinct().ToList();
|
.Select(n => n.SearchTerm)
|
||||||
|
.Distinct(StringComparer.InvariantCultureIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
|
@ -80,7 +80,7 @@ namespace NzbDrone.Core.DecisionEngine
|
||||||
|
|
||||||
if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode)
|
if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode)
|
||||||
{
|
{
|
||||||
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, report.Title, report.TvdbId, report.TvRageId, searchCriteria);
|
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, report.Title, report.TvdbId, report.TvRageId, report.ImdbId, searchCriteria);
|
||||||
|
|
||||||
if (specialEpisodeInfo != null)
|
if (specialEpisodeInfo != null)
|
||||||
{
|
{
|
||||||
|
@ -90,7 +90,7 @@ namespace NzbDrone.Core.DecisionEngine
|
||||||
|
|
||||||
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace())
|
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, searchCriteria);
|
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, report.ImdbId, searchCriteria);
|
||||||
remoteEpisode.Release = report;
|
remoteEpisode.Release = report;
|
||||||
|
|
||||||
if (remoteEpisode.Series == null)
|
if (remoteEpisode.Series == null)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
@ -10,10 +12,13 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators
|
||||||
{
|
{
|
||||||
public class AggregateLanguages : IAggregateRemoteEpisode
|
public class AggregateLanguages : IAggregateRemoteEpisode
|
||||||
{
|
{
|
||||||
|
private readonly IIndexerFactory _indexerFactory;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public AggregateLanguages(Logger logger)
|
public AggregateLanguages(IIndexerFactory indexerFactory,
|
||||||
|
Logger logger)
|
||||||
{
|
{
|
||||||
|
_indexerFactory = indexerFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +76,17 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators
|
||||||
languages = languages.Except(languagesToRemove).ToList();
|
languages = languages.Except(languagesToRemove).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) && releaseInfo is { IndexerId: > 0 } && releaseInfo.Title.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
var indexer = _indexerFactory.Get(releaseInfo.IndexerId);
|
||||||
|
|
||||||
|
if (indexer?.Settings is IIndexerSettings settings && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(releaseInfo.Title))
|
||||||
|
{
|
||||||
|
// Use indexer setting for Multi-languages
|
||||||
|
languages = settings.MultiLanguages.Select(i => (Language)i).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use series language as fallback if we couldn't parse a language
|
// Use series language as fallback if we couldn't parse a language
|
||||||
if (languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown))
|
if (languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown))
|
||||||
{
|
{
|
||||||
|
|
|
@ -129,10 +129,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
||||||
|
|
||||||
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(GetOutputPath(torrent)));
|
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(GetOutputPath(torrent)));
|
||||||
|
|
||||||
yield return new DownloadClientItem
|
var queueItem = new DownloadClientItem
|
||||||
{
|
{
|
||||||
CanMoveFiles = false,
|
|
||||||
CanBeRemoved = torrent.Status == "complete",
|
|
||||||
Category = null,
|
Category = null,
|
||||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||||
DownloadId = torrent.InfoHash?.ToUpper(),
|
DownloadId = torrent.InfoHash?.ToUpper(),
|
||||||
|
@ -146,7 +144,12 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
||||||
Status = status,
|
Status = status,
|
||||||
Title = title,
|
Title = title,
|
||||||
TotalSize = totalLength,
|
TotalSize = totalLength,
|
||||||
|
CanMoveFiles = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == "complete";
|
||||||
|
|
||||||
|
yield return queueItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||||
{
|
{
|
||||||
foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod))
|
foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod))
|
||||||
{
|
{
|
||||||
yield return new DownloadClientItem
|
var queueItem = new DownloadClientItem
|
||||||
{
|
{
|
||||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||||
DownloadId = Definition.Name + "_" + item.DownloadId,
|
DownloadId = Definition.Name + "_" + item.DownloadId,
|
||||||
|
@ -101,11 +101,14 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||||
|
|
||||||
OutputPath = item.OutputPath,
|
OutputPath = item.OutputPath,
|
||||||
|
|
||||||
Status = item.Status,
|
Status = item.Status
|
||||||
|
|
||||||
CanMoveFiles = !Settings.ReadOnly,
|
|
||||||
CanBeRemoved = !Settings.ReadOnly
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
queueItem.CanMoveFiles = queueItem.CanBeRemoved =
|
||||||
|
queueItem.DownloadClientInfo.RemoveCompletedDownloads &&
|
||||||
|
!Settings.ReadOnly;
|
||||||
|
|
||||||
|
yield return queueItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -190,6 +190,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||||
// Here we detect if Deluge is managing the torrent and whether the seed criteria has been met.
|
// Here we detect if Deluge is managing the torrent and whether the seed criteria has been met.
|
||||||
// This allows Sonarr to delete the torrent as appropriate.
|
// This allows Sonarr to delete the torrent as appropriate.
|
||||||
item.CanMoveFiles = item.CanBeRemoved =
|
item.CanMoveFiles = item.CanBeRemoved =
|
||||||
|
item.DownloadClientInfo.RemoveCompletedDownloads &&
|
||||||
torrent.IsAutoManaged &&
|
torrent.IsAutoManaged &&
|
||||||
torrent.StopAtRatio &&
|
torrent.StopAtRatio &&
|
||||||
torrent.Ratio >= torrent.StopRatio &&
|
torrent.Ratio >= torrent.StopRatio &&
|
||||||
|
@ -200,7 +201,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||||
|
|
||||||
if (ignoredCount > 0)
|
if (ignoredCount > 0)
|
||||||
{
|
{
|
||||||
_logger.Warn("{0} torrent(s) were ignored becuase they did not have a title, check Deluge and remove any invalid torrents");
|
_logger.Warn("{0} torrent(s) were ignored because they did not have a title. Check Deluge and remove any invalid torrents");
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|
|
@ -88,7 +88,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = new DownloadClientItem()
|
var item = new DownloadClientItem
|
||||||
{
|
{
|
||||||
Category = Settings.TvCategory,
|
Category = Settings.TvCategory,
|
||||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
|
||||||
|
@ -99,11 +99,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||||
RemainingTime = GetRemainingTime(torrent),
|
RemainingTime = GetRemainingTime(torrent),
|
||||||
SeedRatio = GetSeedRatio(torrent),
|
SeedRatio = GetSeedRatio(torrent),
|
||||||
Status = GetStatus(torrent),
|
Status = GetStatus(torrent),
|
||||||
Message = GetMessage(torrent),
|
Message = GetMessage(torrent)
|
||||||
CanMoveFiles = IsFinished(torrent),
|
|
||||||
CanBeRemoved = IsFinished(torrent)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
item.CanMoveFiles = item.CanBeRemoved = item.DownloadClientInfo.RemoveCompletedDownloads && IsFinished(torrent);
|
||||||
|
|
||||||
if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed)
|
if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed)
|
||||||
{
|
{
|
||||||
item.OutputPath = GetOutputPath(outputPath, torrent, serialNumber);
|
item.OutputPath = GetOutputPath(outputPath, torrent, serialNumber);
|
||||||
|
|
|
@ -153,7 +153,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
||||||
item.Status = DownloadItemStatus.Downloading;
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Status == DownloadItemStatus.Completed)
|
if (item.DownloadClientInfo.RemoveCompletedDownloads && item.Status == DownloadItemStatus.Completed)
|
||||||
{
|
{
|
||||||
// Grab cached seedConfig
|
// Grab cached seedConfig
|
||||||
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(item.DownloadId);
|
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(item.DownloadId);
|
||||||
|
@ -165,7 +165,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
||||||
// Check if seed ratio reached
|
// Check if seed ratio reached
|
||||||
item.CanMoveFiles = item.CanBeRemoved = true;
|
item.CanMoveFiles = item.CanBeRemoved = true;
|
||||||
}
|
}
|
||||||
else if (properties.DateFinished != null && properties.DateFinished > 0)
|
else if (properties.DateFinished is > 0)
|
||||||
{
|
{
|
||||||
// Check if seed time reached
|
// Check if seed time reached
|
||||||
if ((DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds((long)properties.DateFinished)) >= seedConfig.SeedTime)
|
if ((DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds((long)properties.DateFinished)) >= seedConfig.SeedTime)
|
||||||
|
|
|
@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.CanBeRemoved = item.CanMoveFiles = torrent.Status == FreeboxDownloadTaskStatus.Done;
|
item.CanBeRemoved = item.CanMoveFiles = item.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == FreeboxDownloadTaskStatus.Done;
|
||||||
|
|
||||||
queueItems.Add(item);
|
queueItems.Add(item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,10 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
||||||
item.Status = DownloadItemStatus.Downloading;
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.CanMoveFiles = item.CanBeRemoved = torrent.IsFinished && torrent.State == HadoukenTorrentState.Paused;
|
item.CanMoveFiles = item.CanBeRemoved =
|
||||||
|
item.DownloadClientInfo.RemoveCompletedDownloads &&
|
||||||
|
torrent.IsFinished &&
|
||||||
|
torrent.State == HadoukenTorrentState.Paused;
|
||||||
|
|
||||||
items.Add(item);
|
items.Add(item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
|
|
||||||
foreach (var torrent in torrents)
|
foreach (var torrent in torrents)
|
||||||
{
|
{
|
||||||
var item = new DownloadClientItem()
|
var item = new DownloadClientItem
|
||||||
{
|
{
|
||||||
DownloadId = torrent.Hash.ToUpper(),
|
DownloadId = torrent.Hash.ToUpper(),
|
||||||
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
|
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
|
||||||
|
@ -239,7 +239,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
|
|
||||||
// Avoid removing torrents that haven't reached the global max ratio.
|
// Avoid removing torrents that haven't reached the global max ratio.
|
||||||
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
||||||
item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config);
|
item.CanMoveFiles = item.CanBeRemoved =
|
||||||
|
item.DownloadClientInfo.RemoveCompletedDownloads &&
|
||||||
|
torrent.State is "pausedUP" or "stoppedUP" &&
|
||||||
|
HasReachedSeedLimit(torrent, config);
|
||||||
|
|
||||||
switch (torrent.State)
|
switch (torrent.State)
|
||||||
{
|
{
|
||||||
|
|
|
@ -117,7 +117,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||||
item.Status = DownloadItemStatus.Downloading;
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.CanBeRemoved = HasReachedSeedLimit(torrent, item.SeedRatio, configFunc);
|
item.CanBeRemoved = item.DownloadClientInfo.RemoveCompletedDownloads && HasReachedSeedLimit(torrent, item.SeedRatio, configFunc);
|
||||||
item.CanMoveFiles = item.CanBeRemoved && torrent.Status == TransmissionTorrentStatus.Stopped;
|
item.CanMoveFiles = item.CanBeRemoved && torrent.Status == TransmissionTorrentStatus.Stopped;
|
||||||
|
|
||||||
items.Add(item);
|
items.Add(item);
|
||||||
|
|
|
@ -185,7 +185,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||||
// Grab cached seedConfig
|
// Grab cached seedConfig
|
||||||
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(torrent.Hash);
|
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(torrent.Hash);
|
||||||
|
|
||||||
if (torrent.IsFinished && seedConfig != null)
|
if (item.DownloadClientInfo.RemoveCompletedDownloads && torrent.IsFinished && seedConfig != null)
|
||||||
{
|
{
|
||||||
var canRemove = false;
|
var canRemove = false;
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||||
|
|
||||||
// 'Started' without 'Queued' is when the torrent is 'forced seeding'
|
// 'Started' without 'Queued' is when the torrent is 'forced seeding'
|
||||||
item.CanMoveFiles = item.CanBeRemoved =
|
item.CanMoveFiles = item.CanBeRemoved =
|
||||||
|
item.DownloadClientInfo.RemoveCompletedDownloads &&
|
||||||
!torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) &&
|
!torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) &&
|
||||||
!torrent.Status.HasFlag(UTorrentTorrentStatus.Started);
|
!torrent.Status.HasFlag(UTorrentTorrentStatus.Started);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Fluent;
|
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
@ -247,14 +246,14 @@ namespace NzbDrone.Core.Download
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.Debug()
|
_logger.ForDebugEvent()
|
||||||
.Message("No Episodes were just imported, but all episodes were previously imported, possible issue with download history.")
|
.Message("No Episodes were just imported, but all episodes were previously imported, possible issue with download history.")
|
||||||
.Property("SeriesId", trackedDownload.RemoteEpisode.Series.Id)
|
.Property("SeriesId", trackedDownload.RemoteEpisode.Series.Id)
|
||||||
.Property("DownloadId", trackedDownload.DownloadItem.DownloadId)
|
.Property("DownloadId", trackedDownload.DownloadItem.DownloadId)
|
||||||
.Property("Title", trackedDownload.DownloadItem.Title)
|
.Property("Title", trackedDownload.DownloadItem.Title)
|
||||||
.Property("Path", trackedDownload.ImportItem.OutputPath.ToString())
|
.Property("Path", trackedDownload.ImportItem.OutputPath.ToString())
|
||||||
.WriteSentryWarn("DownloadHistoryIncomplete")
|
.WriteSentryWarn("DownloadHistoryIncomplete")
|
||||||
.Write();
|
.Log();
|
||||||
}
|
}
|
||||||
|
|
||||||
var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id));
|
var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id));
|
||||||
|
|
|
@ -37,6 +37,7 @@ namespace NzbDrone.Core.Download
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public bool RemoveCompletedDownloads { get; set; }
|
||||||
public bool HasPostImportCategory { get; set; }
|
public bool HasPostImportCategory { get; set; }
|
||||||
|
|
||||||
public static DownloadClientItemClientInfo FromDownloadClient<TSettings>(
|
public static DownloadClientItemClientInfo FromDownloadClient<TSettings>(
|
||||||
|
@ -49,6 +50,7 @@ namespace NzbDrone.Core.Download
|
||||||
Type = downloadClient.Name,
|
Type = downloadClient.Name,
|
||||||
Id = downloadClient.Definition.Id,
|
Id = downloadClient.Definition.Id,
|
||||||
Name = downloadClient.Definition.Name,
|
Name = downloadClient.Definition.Name,
|
||||||
|
RemoveCompletedDownloads = downloadClient.Definition is DownloadClientDefinition { RemoveCompletedDownloads: true },
|
||||||
HasPostImportCategory = hasPostImportCategory
|
HasPostImportCategory = hasPostImportCategory
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ namespace NzbDrone.Core.Download
|
||||||
public interface IFailedDownloadService
|
public interface IFailedDownloadService
|
||||||
{
|
{
|
||||||
void MarkAsFailed(int historyId, bool skipRedownload = false);
|
void MarkAsFailed(int historyId, bool skipRedownload = false);
|
||||||
void MarkAsFailed(string downloadId, bool skipRedownload = false);
|
void MarkAsFailed(TrackedDownload trackedDownload, bool skipRedownload = false);
|
||||||
void Check(TrackedDownload trackedDownload);
|
void Check(TrackedDownload trackedDownload);
|
||||||
void ProcessFailed(TrackedDownload trackedDownload);
|
void ProcessFailed(TrackedDownload trackedDownload);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ namespace NzbDrone.Core.Download
|
||||||
public class FailedDownloadService : IFailedDownloadService
|
public class FailedDownloadService : IFailedDownloadService
|
||||||
{
|
{
|
||||||
private readonly IHistoryService _historyService;
|
private readonly IHistoryService _historyService;
|
||||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
|
||||||
public FailedDownloadService(IHistoryService historyService,
|
public FailedDownloadService(IHistoryService historyService,
|
||||||
|
@ -28,7 +27,6 @@ namespace NzbDrone.Core.Download
|
||||||
IEventAggregator eventAggregator)
|
IEventAggregator eventAggregator)
|
||||||
{
|
{
|
||||||
_historyService = historyService;
|
_historyService = historyService;
|
||||||
_trackedDownloadService = trackedDownloadService;
|
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,9 +35,10 @@ namespace NzbDrone.Core.Download
|
||||||
var history = _historyService.Get(historyId);
|
var history = _historyService.Get(historyId);
|
||||||
|
|
||||||
var downloadId = history.DownloadId;
|
var downloadId = history.DownloadId;
|
||||||
|
|
||||||
if (downloadId.IsNullOrWhiteSpace())
|
if (downloadId.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
PublishDownloadFailedEvent(new List<EpisodeHistory> { history }, "Manually marked as failed", skipRedownload: skipRedownload);
|
PublishDownloadFailedEvent(history, new List<int> { history.EpisodeId }, "Manually marked as failed", skipRedownload: skipRedownload);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -53,21 +52,19 @@ namespace NzbDrone.Core.Download
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any other history items for the download ID then filter out any duplicate history items.
|
// Add any other history items for the download ID then filter out any duplicate history items.
|
||||||
grabbedHistory.AddRange(_historyService.Find(downloadId, EpisodeHistoryEventType.Grabbed));
|
grabbedHistory.AddRange(GetGrabbedHistory(downloadId));
|
||||||
grabbedHistory = grabbedHistory.DistinctBy(h => h.Id).ToList();
|
grabbedHistory = grabbedHistory.DistinctBy(h => h.Id).ToList();
|
||||||
|
|
||||||
PublishDownloadFailedEvent(grabbedHistory, "Manually marked as failed");
|
PublishDownloadFailedEvent(history, GetEpisodeIds(grabbedHistory), "Manually marked as failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MarkAsFailed(string downloadId, bool skipRedownload = false)
|
public void MarkAsFailed(TrackedDownload trackedDownload, bool skipRedownload = false)
|
||||||
{
|
{
|
||||||
var history = _historyService.Find(downloadId, EpisodeHistoryEventType.Grabbed);
|
var history = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId);
|
||||||
|
|
||||||
if (history.Any())
|
if (history.Any())
|
||||||
{
|
{
|
||||||
var trackedDownload = _trackedDownloadService.Find(downloadId);
|
PublishDownloadFailedEvent(history.First(), GetEpisodeIds(history), "Manually marked as failed", trackedDownload, skipRedownload: skipRedownload);
|
||||||
|
|
||||||
PublishDownloadFailedEvent(history, "Manually marked as failed", trackedDownload, skipRedownload: skipRedownload);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,9 +79,7 @@ namespace NzbDrone.Core.Download
|
||||||
if (trackedDownload.DownloadItem.IsEncrypted ||
|
if (trackedDownload.DownloadItem.IsEncrypted ||
|
||||||
trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
|
trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
|
||||||
{
|
{
|
||||||
var grabbedItems = _historyService
|
var grabbedItems = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId);
|
||||||
.Find(trackedDownload.DownloadItem.DownloadId, EpisodeHistoryEventType.Grabbed)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (grabbedItems.Empty())
|
if (grabbedItems.Empty())
|
||||||
{
|
{
|
||||||
|
@ -103,9 +98,7 @@ namespace NzbDrone.Core.Download
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var grabbedItems = _historyService
|
var grabbedItems = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId);
|
||||||
.Find(trackedDownload.DownloadItem.DownloadId, EpisodeHistoryEventType.Grabbed)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (grabbedItems.Empty())
|
if (grabbedItems.Empty())
|
||||||
{
|
{
|
||||||
|
@ -124,18 +117,17 @@ namespace NzbDrone.Core.Download
|
||||||
}
|
}
|
||||||
|
|
||||||
trackedDownload.State = TrackedDownloadState.Failed;
|
trackedDownload.State = TrackedDownloadState.Failed;
|
||||||
PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload);
|
PublishDownloadFailedEvent(grabbedItems.First(), GetEpisodeIds(grabbedItems), failure, trackedDownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishDownloadFailedEvent(List<EpisodeHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
|
private void PublishDownloadFailedEvent(EpisodeHistory historyItem, List<int> episodeIds, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
|
||||||
{
|
{
|
||||||
var historyItem = historyItems.Last();
|
|
||||||
Enum.TryParse(historyItem.Data.GetValueOrDefault(EpisodeHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
|
Enum.TryParse(historyItem.Data.GetValueOrDefault(EpisodeHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
|
||||||
|
|
||||||
var downloadFailedEvent = new DownloadFailedEvent
|
var downloadFailedEvent = new DownloadFailedEvent
|
||||||
{
|
{
|
||||||
SeriesId = historyItem.SeriesId,
|
SeriesId = historyItem.SeriesId,
|
||||||
EpisodeIds = historyItems.Select(h => h.EpisodeId).Distinct().ToList(),
|
EpisodeIds = episodeIds,
|
||||||
Quality = historyItem.Quality,
|
Quality = historyItem.Quality,
|
||||||
SourceTitle = historyItem.SourceTitle,
|
SourceTitle = historyItem.SourceTitle,
|
||||||
DownloadClient = historyItem.Data.GetValueOrDefault(EpisodeHistory.DOWNLOAD_CLIENT),
|
DownloadClient = historyItem.Data.GetValueOrDefault(EpisodeHistory.DOWNLOAD_CLIENT),
|
||||||
|
@ -145,10 +137,23 @@ namespace NzbDrone.Core.Download
|
||||||
TrackedDownload = trackedDownload,
|
TrackedDownload = trackedDownload,
|
||||||
Languages = historyItem.Languages,
|
Languages = historyItem.Languages,
|
||||||
SkipRedownload = skipRedownload,
|
SkipRedownload = skipRedownload,
|
||||||
ReleaseSource = releaseSource
|
ReleaseSource = releaseSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
_eventAggregator.PublishEvent(downloadFailedEvent);
|
_eventAggregator.PublishEvent(downloadFailedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<int> GetEpisodeIds(List<EpisodeHistory> historyItems)
|
||||||
|
{
|
||||||
|
return historyItems.Select(h => h.EpisodeId).Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EpisodeHistory> GetGrabbedHistory(string downloadId)
|
||||||
|
{
|
||||||
|
// Sort by date so items are always in the same order
|
||||||
|
return _historyService.Find(downloadId, EpisodeHistoryEventType.Grabbed)
|
||||||
|
.OrderByDescending(h => h.Date)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||||
|
|
||||||
if (parsedEpisodeInfo != null)
|
if (parsedEpisodeInfo != null)
|
||||||
{
|
{
|
||||||
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0);
|
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0, null);
|
||||||
|
|
||||||
_aggregationService.Augment(trackedDownload.RemoteEpisode);
|
_aggregationService.Augment(trackedDownload.RemoteEpisode);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||||
// Try parsing the original source title and if that fails, try parsing it as a special
|
// Try parsing the original source title and if that fails, try parsing it as a special
|
||||||
// TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item
|
// TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item
|
||||||
parsedEpisodeInfo = Parser.Parser.ParseTitle(firstHistoryItem.SourceTitle) ??
|
parsedEpisodeInfo = Parser.Parser.ParseTitle(firstHistoryItem.SourceTitle) ??
|
||||||
_parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, firstHistoryItem.SourceTitle, 0, 0);
|
_parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, firstHistoryItem.SourceTitle, 0, 0, null);
|
||||||
|
|
||||||
if (parsedEpisodeInfo != null)
|
if (parsedEpisodeInfo != null)
|
||||||
{
|
{
|
||||||
|
@ -234,7 +234,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||||
{
|
{
|
||||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
|
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
|
||||||
|
|
||||||
trackedDownload.RemoteEpisode = parsedEpisodeInfo == null ? null : _parsingService.Map(parsedEpisodeInfo, 0, 0);
|
trackedDownload.RemoteEpisode = parsedEpisodeInfo == null ? null : _parsingService.Map(parsedEpisodeInfo, 0, 0, null);
|
||||||
|
|
||||||
_aggregationService.Augment(trackedDownload.RemoteEpisode);
|
_aggregationService.Augment(trackedDownload.RemoteEpisode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,26 +20,6 @@ namespace NzbDrone.Core
|
||||||
return actual;
|
return actual;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long Megabytes(this int megabytes)
|
|
||||||
{
|
|
||||||
return Convert.ToInt64(megabytes * 1024L * 1024L);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long Gigabytes(this int gigabytes)
|
|
||||||
{
|
|
||||||
return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long Megabytes(this double megabytes)
|
|
||||||
{
|
|
||||||
return Convert.ToInt64(megabytes * 1024L * 1024L);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long Gigabytes(this double gigabytes)
|
|
||||||
{
|
|
||||||
return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long Round(this long number, long level)
|
public static long Round(this long number, long level)
|
||||||
{
|
{
|
||||||
return Convert.ToInt64(Math.Floor((decimal)number / level) * level);
|
return Convert.ToInt64(Math.Floor((decimal)number / level) * level);
|
||||||
|
|
|
@ -165,6 +165,7 @@ namespace NzbDrone.Core.History
|
||||||
history.Data.Add("Guid", message.Episode.Release.Guid);
|
history.Data.Add("Guid", message.Episode.Release.Guid);
|
||||||
history.Data.Add("TvdbId", message.Episode.Release.TvdbId.ToString());
|
history.Data.Add("TvdbId", message.Episode.Release.TvdbId.ToString());
|
||||||
history.Data.Add("TvRageId", message.Episode.Release.TvRageId.ToString());
|
history.Data.Add("TvRageId", message.Episode.Release.TvRageId.ToString());
|
||||||
|
history.Data.Add("ImdbId", message.Episode.Release.ImdbId);
|
||||||
history.Data.Add("Protocol", ((int)message.Episode.Release.DownloadProtocol).ToString());
|
history.Data.Add("Protocol", ((int)message.Episode.Release.DownloadProtocol).ToString());
|
||||||
history.Data.Add("CustomFormatScore", message.Episode.CustomFormatScore.ToString());
|
history.Data.Add("CustomFormatScore", message.Episode.CustomFormatScore.ToString());
|
||||||
history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString());
|
history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString());
|
||||||
|
|
|
@ -10,7 +10,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||||
{
|
{
|
||||||
public abstract class SearchCriteriaBase
|
public abstract class SearchCriteriaBase
|
||||||
{
|
{
|
||||||
private static readonly Regex SpecialCharacter = new Regex(@"[`'.]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
private static readonly Regex SpecialCharacter = new Regex(@"['.\u0060\u00B4\u2018\u2019]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
|
|
@ -193,7 +193,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
foreach (var item in dict)
|
foreach (var item in dict)
|
||||||
{
|
{
|
||||||
item.Value.Episodes = item.Value.Episodes.Distinct().ToList();
|
item.Value.Episodes = item.Value.Episodes.Distinct().ToList();
|
||||||
item.Value.SceneTitles = item.Value.SceneTitles.Distinct().ToList();
|
item.Value.SceneTitles = item.Value.SceneTitles.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict.Values.ToList();
|
return dict.Values.ToList();
|
||||||
|
@ -221,7 +221,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
|
|
||||||
foreach (var item in dict)
|
foreach (var item in dict)
|
||||||
{
|
{
|
||||||
item.Value.SceneTitles = item.Value.SceneTitles.Distinct().ToList();
|
item.Value.SceneTitles = item.Value.SceneTitles.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict.Values.ToList();
|
return dict.Values.ToList();
|
||||||
|
@ -463,7 +463,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
spec.UserInvokedSearch = userInvokedSearch;
|
spec.UserInvokedSearch = userInvokedSearch;
|
||||||
spec.InteractiveSearch = interactiveSearch;
|
spec.InteractiveSearch = interactiveSearch;
|
||||||
|
|
||||||
if (!spec.SceneTitles.Contains(series.Title))
|
if (!spec.SceneTitles.Contains(series.Title, StringComparer.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
spec.SceneTitles.Add(series.Title);
|
spec.SceneTitles.Add(series.Title);
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,11 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||||
torrentInfo.TvRageId = torrent.TvrageID.Value;
|
torrentInfo.TvRageId = torrent.TvrageID.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (torrent.ImdbID.IsNotNullOrWhiteSpace() && int.TryParse(torrent.ImdbID, out var imdbId) && imdbId > 0)
|
||||||
|
{
|
||||||
|
torrentInfo.ImdbId = $"tt{imdbId:D7}";
|
||||||
|
}
|
||||||
|
|
||||||
results.Add(torrentInfo);
|
results.Add(torrentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||||
{
|
{
|
||||||
var id = result.Id;
|
var id = result.Id;
|
||||||
|
|
||||||
torrentInfos.Add(new TorrentInfo
|
var torrentInfo = new TorrentInfo
|
||||||
{
|
{
|
||||||
Guid = $"FileList-{id}",
|
Guid = $"FileList-{id}",
|
||||||
Title = result.Name,
|
Title = result.Name,
|
||||||
|
@ -48,9 +48,15 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||||
Seeders = result.Seeders,
|
Seeders = result.Seeders,
|
||||||
Peers = result.Leechers + result.Seeders,
|
Peers = result.Leechers + result.Seeders,
|
||||||
PublishDate = result.UploadDate.ToUniversalTime(),
|
PublishDate = result.UploadDate.ToUniversalTime(),
|
||||||
ImdbId = result.ImdbId,
|
|
||||||
IndexerFlags = GetIndexerFlags(result)
|
IndexerFlags = GetIndexerFlags(result)
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (result.ImdbId is { Length: > 2 } && int.TryParse(result.ImdbId.TrimStart('t'), out var imdbId) && imdbId > 0)
|
||||||
|
{
|
||||||
|
torrentInfo.ImdbId = $"tt{imdbId:D7}";
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentInfos.Add(torrentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return torrentInfos.ToArray();
|
return torrentInfos.ToArray();
|
||||||
|
|
|
@ -61,6 +61,7 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||||
Seeders = result.Seeders,
|
Seeders = result.Seeders,
|
||||||
Peers = result.Leechers + result.Seeders,
|
Peers = result.Leechers + result.Seeders,
|
||||||
PublishDate = result.Added.ToUniversalTime(),
|
PublishDate = result.Added.ToUniversalTime(),
|
||||||
|
TvdbId = result.TvdbInfo?.Id ?? 0,
|
||||||
IndexerFlags = GetIndexerFlags(result)
|
IndexerFlags = GetIndexerFlags(result)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
@ -20,8 +19,6 @@ namespace NzbDrone.Core.Indexers
|
||||||
public abstract class IndexerBase<TSettings> : IIndexer
|
public abstract class IndexerBase<TSettings> : IIndexer
|
||||||
where TSettings : IIndexerSettings, new()
|
where TSettings : IIndexerSettings, new()
|
||||||
{
|
{
|
||||||
private static readonly Regex MultiRegex = new (@"[_. ](?<multi>multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
protected readonly IIndexerStatusService _indexerStatusService;
|
protected readonly IIndexerStatusService _indexerStatusService;
|
||||||
protected readonly IConfigService _configService;
|
protected readonly IConfigService _configService;
|
||||||
protected readonly IParsingService _parsingService;
|
protected readonly IParsingService _parsingService;
|
||||||
|
@ -94,7 +91,7 @@ namespace NzbDrone.Core.Indexers
|
||||||
result.ForEach(c =>
|
result.ForEach(c =>
|
||||||
{
|
{
|
||||||
// Use multi languages from setting if ReleaseInfo languages is empty
|
// Use multi languages from setting if ReleaseInfo languages is empty
|
||||||
if (c.Languages.Empty() && MultiRegex.IsMatch(c.Title) && settings.MultiLanguages.Any())
|
if (c.Languages.Empty() && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(c.Title))
|
||||||
{
|
{
|
||||||
c.Languages = settings.MultiLanguages.Select(i => (Language)i).ToList();
|
c.Languages = settings.MultiLanguages.Select(i => (Language)i).ToList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
|
|
||||||
releaseInfo.TvdbId = GetTvdbId(item);
|
releaseInfo.TvdbId = GetTvdbId(item);
|
||||||
releaseInfo.TvRageId = GetTvRageId(item);
|
releaseInfo.TvRageId = GetTvRageId(item);
|
||||||
|
releaseInfo.ImdbId = GetImdbId(item);
|
||||||
|
|
||||||
return releaseInfo;
|
return releaseInfo;
|
||||||
}
|
}
|
||||||
|
@ -182,6 +183,18 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual string GetImdbId(XElement item)
|
||||||
|
{
|
||||||
|
var imdbIdString = TryGetNewznabAttribute(item, "imdb");
|
||||||
|
|
||||||
|
if (!imdbIdString.IsNullOrWhiteSpace() && int.TryParse(imdbIdString, out var imdbId) && imdbId > 0)
|
||||||
|
{
|
||||||
|
return $"tt{imdbId:D7}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected string TryGetNewznabAttribute(XElement item, string key, string defaultValue = "")
|
protected string TryGetNewznabAttribute(XElement item, string key, string defaultValue = "")
|
||||||
{
|
{
|
||||||
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
|
@ -83,6 +83,7 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||||
{
|
{
|
||||||
torrentInfo.TvdbId = GetTvdbId(item);
|
torrentInfo.TvdbId = GetTvdbId(item);
|
||||||
torrentInfo.TvRageId = GetTvRageId(item);
|
torrentInfo.TvRageId = GetTvRageId(item);
|
||||||
|
releaseInfo.ImdbId = GetImdbId(item);
|
||||||
torrentInfo.IndexerFlags = GetFlags(item);
|
torrentInfo.IndexerFlags = GetFlags(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +178,18 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual string GetImdbId(XElement item)
|
||||||
|
{
|
||||||
|
var imdbIdString = TryGetTorznabAttribute(item, "imdb");
|
||||||
|
|
||||||
|
if (!imdbIdString.IsNullOrWhiteSpace() && int.TryParse(imdbIdString, out var imdbId) && imdbId > 0)
|
||||||
|
{
|
||||||
|
return $"tt{imdbId:D7}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected override string GetInfoHash(XElement item)
|
protected override string GetInfoHash(XElement item)
|
||||||
{
|
{
|
||||||
return TryGetTorznabAttribute(item, "infohash");
|
return TryGetTorznabAttribute(item, "infohash");
|
||||||
|
|
|
@ -33,22 +33,25 @@ namespace NzbDrone.Core.Instrumentation
|
||||||
|
|
||||||
LogManager.Configuration.AddTarget("DbLogger", target);
|
LogManager.Configuration.AddTarget("DbLogger", target);
|
||||||
LogManager.Configuration.LoggingRules.Add(Rule);
|
LogManager.Configuration.LoggingRules.Add(Rule);
|
||||||
LogManager.ConfigurationReloaded += OnLogManagerOnConfigurationReloaded;
|
LogManager.ConfigurationChanged += OnLogManagerOnConfigurationReloaded;
|
||||||
LogManager.ReconfigExistingLoggers();
|
LogManager.ReconfigExistingLoggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnRegister()
|
public void UnRegister()
|
||||||
{
|
{
|
||||||
LogManager.ConfigurationReloaded -= OnLogManagerOnConfigurationReloaded;
|
LogManager.ConfigurationChanged -= OnLogManagerOnConfigurationReloaded;
|
||||||
LogManager.Configuration.RemoveTarget("DbLogger");
|
LogManager.Configuration.RemoveTarget("DbLogger");
|
||||||
LogManager.Configuration.LoggingRules.Remove(Rule);
|
LogManager.Configuration.LoggingRules.Remove(Rule);
|
||||||
LogManager.ReconfigExistingLoggers();
|
LogManager.ReconfigExistingLoggers();
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLogManagerOnConfigurationReloaded(object sender, LoggingConfigurationReloadedEventArgs args)
|
private void OnLogManagerOnConfigurationReloaded(object sender, LoggingConfigurationChangedEventArgs args)
|
||||||
{
|
{
|
||||||
Register();
|
if (args.ActivatedConfiguration != null)
|
||||||
|
{
|
||||||
|
Register();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoggingRule Rule { get; set; }
|
public LoggingRule Rule { get; set; }
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Config;
|
using NLog.Config;
|
||||||
|
using NLog.Targets;
|
||||||
using NLog.Targets.Syslog;
|
using NLog.Targets.Syslog;
|
||||||
using NLog.Targets.Syslog.Settings;
|
using NLog.Targets.Syslog.Settings;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
@ -51,13 +52,14 @@ namespace NzbDrone.Core.Instrumentation
|
||||||
var rules = LogManager.Configuration.LoggingRules;
|
var rules = LogManager.Configuration.LoggingRules;
|
||||||
|
|
||||||
// Console
|
// Console
|
||||||
|
ReconfigureConsole();
|
||||||
SetMinimumLogLevel(rules, "consoleLogger", minimumConsoleLogLevel);
|
SetMinimumLogLevel(rules, "consoleLogger", minimumConsoleLogLevel);
|
||||||
|
|
||||||
// Log Files
|
// Log Files
|
||||||
SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off);
|
SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off);
|
||||||
SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off);
|
SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off);
|
||||||
SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off);
|
SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off);
|
||||||
SetLogRotation();
|
ReconfigureFile();
|
||||||
|
|
||||||
// Log Sql
|
// Log Sql
|
||||||
SqlBuilderExtensions.LogSql = _configFileProvider.LogSql;
|
SqlBuilderExtensions.LogSql = _configFileProvider.LogSql;
|
||||||
|
@ -91,11 +93,12 @@ namespace NzbDrone.Core.Instrumentation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetLogRotation()
|
private void ReconfigureFile()
|
||||||
{
|
{
|
||||||
foreach (var target in LogManager.Configuration.AllTargets.OfType<NzbDroneFileTarget>())
|
foreach (var target in LogManager.Configuration.AllTargets.OfType<NzbDroneFileTarget>())
|
||||||
{
|
{
|
||||||
target.MaxArchiveFiles = _configFileProvider.LogRotate;
|
target.MaxArchiveFiles = _configFileProvider.LogRotate;
|
||||||
|
target.ArchiveAboveSize = _configFileProvider.LogSizeLimit.Megabytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +112,22 @@ namespace NzbDrone.Core.Instrumentation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReconfigureConsole()
|
||||||
|
{
|
||||||
|
var consoleTarget = LogManager.Configuration.AllTargets.OfType<ColoredConsoleTarget>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (consoleTarget != null)
|
||||||
|
{
|
||||||
|
var format = _configFileProvider.ConsoleLogFormat;
|
||||||
|
|
||||||
|
consoleTarget.Layout = format switch
|
||||||
|
{
|
||||||
|
ConsoleLogFormat.Clef => NzbDroneLogger.ClefLogLayout,
|
||||||
|
_ => NzbDroneLogger.ConsoleLogLayout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SetSyslogParameters(string syslogServer, int syslogPort, LogLevel minimumLogLevel)
|
private void SetSyslogParameters(string syslogServer, int syslogPort, LogLevel minimumLogLevel)
|
||||||
{
|
{
|
||||||
var syslogTarget = new SyslogTarget();
|
var syslogTarget = new SyslogTarget();
|
||||||
|
@ -117,7 +136,7 @@ namespace NzbDrone.Core.Instrumentation
|
||||||
syslogTarget.MessageSend.Protocol = ProtocolType.Udp;
|
syslogTarget.MessageSend.Protocol = ProtocolType.Udp;
|
||||||
syslogTarget.MessageSend.Udp.Port = syslogPort;
|
syslogTarget.MessageSend.Udp.Port = syslogPort;
|
||||||
syslogTarget.MessageSend.Udp.Server = syslogServer;
|
syslogTarget.MessageSend.Udp.Server = syslogServer;
|
||||||
syslogTarget.MessageSend.Udp.ReconnectInterval = 500;
|
syslogTarget.MessageSend.Retry.ConstantBackoff.BaseDelay = 500;
|
||||||
syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424;
|
syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424;
|
||||||
syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName;
|
syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
{
|
{
|
||||||
"AddAutoTag": "أضف كلمات دلالية تلقائيا",
|
"AddAutoTag": "أضف كلمات دلالية تلقائيا",
|
||||||
"AddCondition": "إضافة شرط"
|
"AddCondition": "إضافة شرط",
|
||||||
|
"AutoTaggingNegateHelpText": "إذا تم تحديده ، فلن يتم تطبيق التنسيق المخصص إذا تطابق شرط {implementationName} هذا.",
|
||||||
|
"ConnectionLostReconnect": "سيحاول {appName} الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1325,8 +1325,8 @@
|
||||||
"NotificationsEmailSettingsUseEncryption": "Use Encryption",
|
"NotificationsEmailSettingsUseEncryption": "Use Encryption",
|
||||||
"NotificationsEmailSettingsUseEncryptionHelpText": "Whether to prefer using encryption if configured on the server, to always use encryption via SSL (Port 465 only) or StartTLS (any other port) or to never use encryption",
|
"NotificationsEmailSettingsUseEncryptionHelpText": "Whether to prefer using encryption if configured on the server, to always use encryption via SSL (Port 465 only) or StartTLS (any other port) or to never use encryption",
|
||||||
"NotificationsEmbySettingsSendNotifications": "Send Notifications",
|
"NotificationsEmbySettingsSendNotifications": "Send Notifications",
|
||||||
"NotificationsEmbySettingsSendNotificationsHelpText": "Have MediaBrowser send notifications to configured providers",
|
"NotificationsEmbySettingsSendNotificationsHelpText": "Have Emby send notifications to configured providers. Not supported on Jellyfin.",
|
||||||
"NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete?",
|
"NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete",
|
||||||
"NotificationsGotifySettingIncludeSeriesPoster": "Include Series Poster",
|
"NotificationsGotifySettingIncludeSeriesPoster": "Include Series Poster",
|
||||||
"NotificationsGotifySettingIncludeSeriesPosterHelpText": "Include series poster in message",
|
"NotificationsGotifySettingIncludeSeriesPosterHelpText": "Include series poster in message",
|
||||||
"NotificationsGotifySettingsAppToken": "App Token",
|
"NotificationsGotifySettingsAppToken": "App Token",
|
||||||
|
@ -1429,6 +1429,8 @@
|
||||||
"NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages",
|
"NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages",
|
||||||
"NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title",
|
"NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title",
|
||||||
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications",
|
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications",
|
||||||
|
"NotificationsTelegramSettingsMetadataLinks": "Metadata Links",
|
||||||
|
"NotificationsTelegramSettingsMetadataLinksHelpText": "Add a links to series metadata when sending notifications",
|
||||||
"NotificationsTelegramSettingsSendSilently": "Send Silently",
|
"NotificationsTelegramSettingsSendSilently": "Send Silently",
|
||||||
"NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound",
|
"NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound",
|
||||||
"NotificationsTelegramSettingsTopicId": "Topic ID",
|
"NotificationsTelegramSettingsTopicId": "Topic ID",
|
||||||
|
|
|
@ -1401,7 +1401,7 @@
|
||||||
"NotificationsEmailSettingsBccAddressHelpText": "Lista separada por coma de destinatarios de e-mail bcc",
|
"NotificationsEmailSettingsBccAddressHelpText": "Lista separada por coma de destinatarios de e-mail bcc",
|
||||||
"NotificationsEmailSettingsName": "E-mail",
|
"NotificationsEmailSettingsName": "E-mail",
|
||||||
"NotificationsEmailSettingsRecipientAddress": "Dirección(es) de destinatario",
|
"NotificationsEmailSettingsRecipientAddress": "Dirección(es) de destinatario",
|
||||||
"NotificationsEmbySettingsSendNotificationsHelpText": "Hacer que MediaBrowser envíe notificaciones a los proveedores configurados",
|
"NotificationsEmbySettingsSendNotificationsHelpText": "Hace que Emby envíe notificaciones a los proveedores configurados. No soportado en Jellyfin.",
|
||||||
"NotificationsGotifySettingsAppToken": "Token de app",
|
"NotificationsGotifySettingsAppToken": "Token de app",
|
||||||
"NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluye poster de serie en mensaje",
|
"NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluye poster de serie en mensaje",
|
||||||
"NotificationsJoinSettingsDeviceNames": "Nombres de dispositivo",
|
"NotificationsJoinSettingsDeviceNames": "Nombres de dispositivo",
|
||||||
|
@ -1839,7 +1839,7 @@
|
||||||
"Titles": "Títulos",
|
"Titles": "Títulos",
|
||||||
"ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar",
|
"ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar",
|
||||||
"TotalFileSize": "Tamaño total de archivo",
|
"TotalFileSize": "Tamaño total de archivo",
|
||||||
"UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización",
|
"UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización: {version}",
|
||||||
"UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado",
|
"UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado",
|
||||||
"UrlBase": "URL base",
|
"UrlBase": "URL base",
|
||||||
"UseSsl": "Usar SSL",
|
"UseSsl": "Usar SSL",
|
||||||
|
@ -1919,7 +1919,7 @@
|
||||||
"NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord",
|
"NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord",
|
||||||
"NotificationsEmailSettingsCcAddress": "Dirección(es) CC",
|
"NotificationsEmailSettingsCcAddress": "Dirección(es) CC",
|
||||||
"NotificationsEmbySettingsSendNotifications": "Enviar notificaciones",
|
"NotificationsEmbySettingsSendNotifications": "Enviar notificaciones",
|
||||||
"NotificationsEmbySettingsUpdateLibraryHelpText": "¿Actualiza biblioteca en importar, renombrar o borrar?",
|
"NotificationsEmbySettingsUpdateLibraryHelpText": "Actualiza biblioteca al importar, renombrar o borrar",
|
||||||
"NotificationsJoinSettingsDeviceIdsHelpText": "En desuso, usar Nombres de dispositivo en su lugar. Lista separada por coma de los IDs de dispositivo a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.",
|
"NotificationsJoinSettingsDeviceIdsHelpText": "En desuso, usar Nombres de dispositivo en su lugar. Lista separada por coma de los IDs de dispositivo a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.",
|
||||||
"NotificationsPushoverSettingsExpire": "Caduca",
|
"NotificationsPushoverSettingsExpire": "Caduca",
|
||||||
"NotificationsMailgunSettingsSenderDomain": "Dominio del remitente",
|
"NotificationsMailgunSettingsSenderDomain": "Dominio del remitente",
|
||||||
|
@ -2093,5 +2093,12 @@
|
||||||
"CountVotes": "{votes} votos",
|
"CountVotes": "{votes} votos",
|
||||||
"InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?",
|
"InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?",
|
||||||
"InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información.",
|
"InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información.",
|
||||||
"NextAiringDate": "Siguiente emisión: {date}"
|
"NextAiringDate": "Siguiente emisión: {date}",
|
||||||
|
"SeasonsMonitoredAll": "Todas",
|
||||||
|
"SeasonsMonitoredNone": "Ninguna",
|
||||||
|
"SeasonsMonitoredStatus": "Temporadas monitorizadas",
|
||||||
|
"NoBlocklistItems": "Ningún elemento en la lista de bloqueo",
|
||||||
|
"SeasonsMonitoredPartial": "Parcial",
|
||||||
|
"NotificationsTelegramSettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones",
|
||||||
|
"NotificationsTelegramSettingsMetadataLinks": "Enlaces de metadatos"
|
||||||
}
|
}
|
||||||
|
|
|
@ -299,7 +299,7 @@
|
||||||
"Sunday": "Dimanche",
|
"Sunday": "Dimanche",
|
||||||
"TorrentDelay": "Retard du torrent",
|
"TorrentDelay": "Retard du torrent",
|
||||||
"DownloadClients": "Clients de télécharg.",
|
"DownloadClients": "Clients de télécharg.",
|
||||||
"CustomFormats": "Formats perso.",
|
"CustomFormats": "Formats personnalisés",
|
||||||
"NoIndexersFound": "Aucun indexeur n'a été trouvé",
|
"NoIndexersFound": "Aucun indexeur n'a été trouvé",
|
||||||
"Profiles": "Profils",
|
"Profiles": "Profils",
|
||||||
"Dash": "Tiret",
|
"Dash": "Tiret",
|
||||||
|
@ -431,7 +431,7 @@
|
||||||
"Replace": "Remplacer",
|
"Replace": "Remplacer",
|
||||||
"ResetAPIKeyMessageText": "Êtes-vous sûr de vouloir réinitialiser votre clé API ?",
|
"ResetAPIKeyMessageText": "Êtes-vous sûr de vouloir réinitialiser votre clé API ?",
|
||||||
"StopSelecting": "Effacer la sélection",
|
"StopSelecting": "Effacer la sélection",
|
||||||
"WhatsNew": "Quoi de neuf ?",
|
"WhatsNew": "Quoi de neuf ?",
|
||||||
"EditDownloadClientImplementation": "Modifier le client de téléchargement - {implementationName}",
|
"EditDownloadClientImplementation": "Modifier le client de téléchargement - {implementationName}",
|
||||||
"External": "Externe",
|
"External": "Externe",
|
||||||
"Monday": "Lundi",
|
"Monday": "Lundi",
|
||||||
|
@ -452,7 +452,7 @@
|
||||||
"RootFolderSelectFreeSpace": "{freeSpace} Libre",
|
"RootFolderSelectFreeSpace": "{freeSpace} Libre",
|
||||||
"WantMoreControlAddACustomFormat": "Vous souhaitez avoir plus de contrôle sur les téléchargements préférés ? Ajoutez un [Format personnalisé](/settings/customformats)",
|
"WantMoreControlAddACustomFormat": "Vous souhaitez avoir plus de contrôle sur les téléchargements préférés ? Ajoutez un [Format personnalisé](/settings/customformats)",
|
||||||
"RemoveSelectedItemsQueueMessageText": "Voulez-vous vraiment supprimer {selectedCount} éléments de la file d'attente ?",
|
"RemoveSelectedItemsQueueMessageText": "Voulez-vous vraiment supprimer {selectedCount} éléments de la file d'attente ?",
|
||||||
"UpdateAll": "Tout actualiser",
|
"UpdateAll": "Tout mettre à jour",
|
||||||
"EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif",
|
"EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif",
|
||||||
"UnmonitorDeletedEpisodesHelpText": "Les épisodes effacés du disque dur ne seront plus surveillés dans {appName}",
|
"UnmonitorDeletedEpisodesHelpText": "Les épisodes effacés du disque dur ne seront plus surveillés dans {appName}",
|
||||||
"RssSync": "Synchronisation RSS",
|
"RssSync": "Synchronisation RSS",
|
||||||
|
@ -1130,7 +1130,7 @@
|
||||||
"NotificationsTagsSeriesHelpText": "N'envoyer des notifications que pour les séries avec au moins une balise correspondante",
|
"NotificationsTagsSeriesHelpText": "N'envoyer des notifications que pour les séries avec au moins une balise correspondante",
|
||||||
"OnApplicationUpdate": "Lors de la mise à jour de l'application",
|
"OnApplicationUpdate": "Lors de la mise à jour de l'application",
|
||||||
"OnEpisodeFileDelete": "Lors de la suppression du fichier de l'épisode",
|
"OnEpisodeFileDelete": "Lors de la suppression du fichier de l'épisode",
|
||||||
"OnHealthIssue": "Sur la question de la santé",
|
"OnHealthIssue": "Lors de problème de santé",
|
||||||
"OnManualInteractionRequired": "Sur l'interaction manuelle requise",
|
"OnManualInteractionRequired": "Sur l'interaction manuelle requise",
|
||||||
"OnRename": "Au renommage",
|
"OnRename": "Au renommage",
|
||||||
"PreferredSize": "Taille préférée",
|
"PreferredSize": "Taille préférée",
|
||||||
|
@ -1176,7 +1176,7 @@
|
||||||
"Total": "Total",
|
"Total": "Total",
|
||||||
"Upcoming": "À venir",
|
"Upcoming": "À venir",
|
||||||
"UpdateAutomaticallyHelpText": "Téléchargez et installez automatiquement les mises à jour. Vous pourrez toujours installer à partir du système : mises à jour",
|
"UpdateAutomaticallyHelpText": "Téléchargez et installez automatiquement les mises à jour. Vous pourrez toujours installer à partir du système : mises à jour",
|
||||||
"UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible",
|
"UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible : {version}",
|
||||||
"UpdateFiltered": "Mise à jour filtrée",
|
"UpdateFiltered": "Mise à jour filtrée",
|
||||||
"IconForSpecialsHelpText": "Afficher l'icône pour les épisodes spéciaux (saison 0)",
|
"IconForSpecialsHelpText": "Afficher l'icône pour les épisodes spéciaux (saison 0)",
|
||||||
"Ignored": "Ignoré",
|
"Ignored": "Ignoré",
|
||||||
|
@ -1276,7 +1276,7 @@
|
||||||
"ConnectSettingsSummary": "Notifications, connexions aux serveurs/lecteurs de médias et scripts personnalisés",
|
"ConnectSettingsSummary": "Notifications, connexions aux serveurs/lecteurs de médias et scripts personnalisés",
|
||||||
"CopyToClipboard": "Copier dans le presse-papier",
|
"CopyToClipboard": "Copier dans le presse-papier",
|
||||||
"CreateEmptySeriesFolders": "Créer des dossiers de séries vides",
|
"CreateEmptySeriesFolders": "Créer des dossiers de séries vides",
|
||||||
"Custom": "Customisé",
|
"Custom": "Personnaliser",
|
||||||
"CopyUsingHardlinksSeriesHelpText": "Les liens physiques permettent à {appName} d'importer des torrents dans le dossier de la série sans prendre d'espace disque supplémentaire ni copier l'intégralité du contenu du fichier. Les liens physiques ne fonctionneront que si la source et la destination sont sur le même volume",
|
"CopyUsingHardlinksSeriesHelpText": "Les liens physiques permettent à {appName} d'importer des torrents dans le dossier de la série sans prendre d'espace disque supplémentaire ni copier l'intégralité du contenu du fichier. Les liens physiques ne fonctionneront que si la source et la destination sont sur le même volume",
|
||||||
"CustomFormatsSettingsSummary": "Formats et paramètres personnalisés",
|
"CustomFormatsSettingsSummary": "Formats et paramètres personnalisés",
|
||||||
"CustomFormatsSettings": "Paramètre des formats personnalisés",
|
"CustomFormatsSettings": "Paramètre des formats personnalisés",
|
||||||
|
@ -1724,8 +1724,8 @@
|
||||||
"NotificationsGotifySettingsPriorityHelpText": "Priorité de la notification",
|
"NotificationsGotifySettingsPriorityHelpText": "Priorité de la notification",
|
||||||
"NotificationsGotifySettingsAppTokenHelpText": "Le jeton d'application généré par Gotify",
|
"NotificationsGotifySettingsAppTokenHelpText": "Le jeton d'application généré par Gotify",
|
||||||
"NotificationsGotifySettingsAppToken": "Jeton d'app",
|
"NotificationsGotifySettingsAppToken": "Jeton d'app",
|
||||||
"NotificationsEmbySettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque lors d'import, de renommage ou de suppression ?",
|
"NotificationsEmbySettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque lors de l'importation, du changement de nom ou de la suppression",
|
||||||
"NotificationsEmbySettingsSendNotificationsHelpText": "Faire en sorte que MediaBrowser envoie des notifications aux fournisseurs configurés",
|
"NotificationsEmbySettingsSendNotificationsHelpText": "Demandez à Emby d'envoyer des notifications aux fournisseurs configurés. Non pris en charge sur Jellyfin.",
|
||||||
"NotificationsEmbySettingsSendNotifications": "Envoyer des notifications",
|
"NotificationsEmbySettingsSendNotifications": "Envoyer des notifications",
|
||||||
"NotificationsEmailSettingsServerHelpText": "Nom d'hôte ou adresse IP du serveur de courriel",
|
"NotificationsEmailSettingsServerHelpText": "Nom d'hôte ou adresse IP du serveur de courriel",
|
||||||
"NotificationsEmailSettingsServer": "Serveur",
|
"NotificationsEmailSettingsServer": "Serveur",
|
||||||
|
@ -2076,5 +2076,29 @@
|
||||||
"UnableToImportAutomatically": "Impossible d'importer automatiquement",
|
"UnableToImportAutomatically": "Impossible d'importer automatiquement",
|
||||||
"DayOfWeekAt": "{day} à {time}",
|
"DayOfWeekAt": "{day} à {time}",
|
||||||
"TomorrowAt": "Demain à {time}",
|
"TomorrowAt": "Demain à {time}",
|
||||||
"TodayAt": "Aujourd'hui à {time}"
|
"TodayAt": "Aujourd'hui à {time}",
|
||||||
|
"ShowTagsHelpText": "Afficher les labels sous l'affiche",
|
||||||
|
"ShowTags": "Afficher les labels",
|
||||||
|
"CountVotes": "{votes} votes",
|
||||||
|
"NoBlocklistItems": "Aucun élément de la liste de blocage",
|
||||||
|
"NotificationsTelegramSettingsMetadataLinksHelpText": "Ajouter un lien vers les métadonnées de la série lors de l'envoi de notifications",
|
||||||
|
"RatingVotes": "Votes de notation",
|
||||||
|
"OnFileImport": "Lors de l'importation du fichier",
|
||||||
|
"OnImportComplete": "Une fois l'importation terminée",
|
||||||
|
"NotificationsPlexSettingsServer": "Serveur",
|
||||||
|
"NotificationsPlexSettingsServerHelpText": "Sélectionnez le serveur à partir du compte plex.tv après l'authentification",
|
||||||
|
"OnFileUpgrade": "Lors de la mise à jour du fichier",
|
||||||
|
"NextAiringDate": "Prochaine diffusion : {date}",
|
||||||
|
"CustomColonReplacement": "Remplacement personnalisé des deux‐points",
|
||||||
|
"CustomColonReplacementFormatHelpText": "Caractères à utiliser en remplacement des deux-points",
|
||||||
|
"CustomColonReplacementFormatHint": "Caractère valide du système de fichiers tel que deux-points (lettre)",
|
||||||
|
"Install": "Installer",
|
||||||
|
"InstallMajorVersionUpdate": "Installer la mise à jour",
|
||||||
|
"InstallMajorVersionUpdateMessage": "Cette mise à jour installera une nouvelle version majeure et pourrait ne pas être compatible avec votre système. Êtes-vous sûr de vouloir installer cette mise à jour ?",
|
||||||
|
"InstallMajorVersionUpdateMessageLink": "Veuillez consulter [{domain}]({url}) pour plus d'informations.",
|
||||||
|
"SeasonsMonitoredAll": "Toutes",
|
||||||
|
"SeasonsMonitoredPartial": "Partielle",
|
||||||
|
"SeasonsMonitoredNone": "Aucune",
|
||||||
|
"SeasonsMonitoredStatus": "Saisons surveillées",
|
||||||
|
"NotificationsTelegramSettingsMetadataLinks": "Liens de métadonnées"
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue