Compare commits

..

32 Commits

Author SHA1 Message Date
Bogdan 2f04b037a1 Fixed nlog deprecated calls 2024-08-11 09:08:38 -07:00
Bogdan 7b87de2e93
Clear pending changes for edit import list exclusions on modal close 2024-08-11 11:53:17 -04:00
Bogdan eb2fd13509
Fixed: Overwriting query params for remove item handler (#7075) 2024-08-11 11:51:11 -04:00
Bogdan ffdb08cfe6 Fixed: Dedupe titles to avoid similar search requests 2024-08-11 08:49:22 -07:00
Mark McDowall 37c4647f24 Fix typos and improve log messages 2024-08-11 08:48:33 -07:00
Mark McDowall f7a58aab33 Align queue action buttons on right 2024-08-11 08:48:33 -07:00
Mark McDowall 4b186e894e
Fixed: Marking queued item as failed not blocking the correct Torrent Info Hash 2024-08-11 11:48:22 -04:00
kephasdev 35a2bc9403
Fix: Use indexer's Multi Languages setting for pushed releases
Closes #7059
2024-08-11 11:47:59 -04:00
Bogdan cc03ce04f1 Fixed: Formatting empty size on disk values 2024-08-11 08:46:56 -07:00
Bogdan 363f8fc347
New: Match search releases using IMDb ID if available 2024-08-11 11:46:46 -04:00
RaZaSB 0877a6718d
New: Remove all single quote characters from searches 2024-08-11 11:46:02 -04:00
Bogdan 8b253c36ea
Validation for bulk series editor 2024-08-11 11:45:15 -04:00
Bogdan e6f82270a9
Parse TVDB ID for releases from HDBits
ignore-downstream
2024-08-11 11:45:00 -04:00
Mark McDowall 813965e6a2 New: Configurable log file size limit 2024-08-11 08:44:35 -07:00
Mark McDowall 0d914f4c53 New: Add Compact Log Event Format option for console logging
Closes #7045
2024-08-11 08:44:35 -07:00
Mark McDowall ae7f73208a Upgrade nlog to 5.3.2 2024-08-11 08:44:35 -07:00
Weblate 4c86d673ea Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-08-11 08:44:27 -07:00
Weblate b1527f9abb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: iMohmmedSA <i.mohmmed.i+1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-31 22:26:09 -07:00
Bogdan 291d792810
Fixed: Moving files on import for usenet clients
Closes #7043
2024-08-01 01:17:10 -04:00
Mark McDowall 9b528eb829
New: Default file log level changed to debug 2024-08-01 01:16:24 -04:00
Mark McDowall 4c0b896174 Improve messaging for for Send Notifications setting in Emby / Jellyfin
Closes #7042
2024-07-31 22:16:01 -07:00
Bogdan 4ff83f9efc
Fixed: Persist Indexer Flags for automatic imports
Revert "Fixed: Persist Indexer Flags when manual importing from queue"

This reverts commit 217611d716.
2024-08-01 01:15:36 -04:00
Bogdan 217611d716
Fixed: Persist Indexer Flags when manual importing from queue 2024-07-31 00:28:01 -04:00
Mark McDowall 1299a97579 Update React Lint rules for TSX 2024-07-30 21:27:33 -07:00
Mark McDowall 4c0de55672 Fixed: Setting page size in Queue, History and Blocklist
Closes #7035
2024-07-30 21:27:33 -07:00
Bogdan 78a0def46a
Fixed: Moving files for torrents when Remove Completed is disabled 2024-07-31 00:27:19 -04:00
Mark McDowall 11a9dcb389
New: Return downloading magnets from Transmission
Closes #7029
2024-07-31 00:26:24 -04:00
Mark McDowall 4eab168267
New: Add metadata links to telegram messages
Closes #5342
---------

Co-authored-by: Ivar Stangeby <istangeby@gmail.com>
2024-07-31 00:25:48 -04:00
Bogdan c9b5a1258a New: Title filter for Series Index 2024-07-30 21:25:10 -07:00
Mark McDowall 9127a91dfc Fixed: Allow leading/trailing spaces on non-Windows
Closes #6971
2024-07-30 21:25:00 -07:00
Weblate cc85a28ff7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Wolfy The Broccoly <theproviderofsolace@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-30 21:24:50 -07:00
Mark McDowall 72db8099e0 Convert System to TypeScript 2024-07-28 17:47:08 -07:00
133 changed files with 958 additions and 412 deletions

View File

@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
app_guid=${app_guid:-media}
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
# Create User / Group as needed
@ -114,7 +114,7 @@ case "$ARCH" in
esac
echo ""
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
echo ""
echo "Downloading..."

View File

@ -359,11 +359,16 @@ module.exports = {
],
rules: Object.assign(typescriptEslintRecommended.rules, {
'no-shadow': 'off',
// These should be enabled after cleaning things up
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'no-shadow': 'off',
'prettier/prettier': 'error',
'simple-import-sort/imports': [
'error',
@ -376,7 +381,41 @@ module.exports = {
['^@?\\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'
})
},
{

View File

@ -59,6 +59,7 @@ function Blocklist() {
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isRemoving,
@ -223,6 +224,7 @@ function Blocklist() {
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
@ -264,6 +266,7 @@ function Blocklist() {
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}

View File

@ -53,6 +53,7 @@ function History() {
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
} = useSelector((state: AppState) => state.history);
@ -154,6 +155,7 @@ function History() {
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
@ -193,6 +195,7 @@ function History() {
<div>
<Table
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}

View File

@ -73,6 +73,7 @@ function Queue() {
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isGrabbing,
@ -269,8 +270,10 @@ function Queue() {
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
@ -344,6 +347,7 @@ function Queue() {
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
maxPageSize={200}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}

View File

@ -1,11 +1,11 @@
import React, { Fragment, useCallback } from 'react';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { setQueueOption } from 'Store/Actions/queueActions';
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
@ -22,24 +22,26 @@ function QueueOptions() {
[name]: value,
})
);
if (name === 'includeUnknownSeriesItems') {
dispatch(gotoQueuePage({ page: 1 }));
}
},
[dispatch]
);
return (
<Fragment>
<FormGroup>
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
<FormGroup>
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownSeriesItems"
value={includeUnknownSeriesItems}
helpText={translate('ShowUnknownSeriesItemsHelpText')}
onChange={handleOptionChange}
/>
</FormGroup>
</Fragment>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownSeriesItems"
value={includeUnknownSeriesItems}
helpText={translate('ShowUnknownSeriesItemsHelpText')}
onChange={handleOptionChange}
/>
</FormGroup>
);
}

View File

@ -26,4 +26,5 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
text-align: right;
}

View File

@ -35,6 +35,10 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
function RedirectWithUrlBase() {
return <Redirect to={getPathWithUrlBase('/')} />;
}
function AppRoutes() {
return (
<Switch>
@ -51,9 +55,7 @@ function AppRoutes() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
addUrlBase={false}
render={() => {
return <Redirect to={getPathWithUrlBase('/')} />;
}}
render={RedirectWithUrlBase}
/>
)}
@ -61,21 +63,9 @@ function AppRoutes() {
<Route path="/add/import" component={ImportSeries} />
<Route
path="/serieseditor"
exact={true}
render={() => {
return <Redirect to={getPathWithUrlBase('/')} />;
}}
/>
<Route path="/serieseditor" exact={true} render={RedirectWithUrlBase} />
<Route
path="/seasonpass"
exact={true}
render={() => {
return <Redirect to={getPathWithUrlBase('/')} />;
}}
/>
<Route path="/seasonpass" exact={true} render={RedirectWithUrlBase} />
<Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} />

View File

@ -64,7 +64,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
<div>{info.componentStack}</div>
)}
{<div className={styles.version}>Version: {window.Sonarr.version}</div>}
<div className={styles.version}>Version: {window.Sonarr.version}</div>
</details>
</div>
);

View File

@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
@ -82,9 +82,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
<Popover
anchor={
<span>
{showSeasonNumber && seasonNumber != null && (
<Fragment>{seasonNumber}x</Fragment>
)}
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
@ -111,9 +109,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
/>
) : (
<span>
{showSeasonNumber && seasonNumber != null && (
<Fragment>{seasonNumber}x</Fragment>
)}
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}

View File

@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
export default function useModalOpenState(
initialState: boolean
): [boolean, () => void, () => void] {
const [isOpen, setOpen] = useState(initialState);
const [isOpen, setIsOpen] = useState(initialState);
const setModalOpen = useCallback(() => {
setOpen(true);
}, [setOpen]);
setIsOpen(true);
}, [setIsOpen]);
const setModalClosed = useCallback(() => {
setOpen(false);
}, [setOpen]);
setIsOpen(false);
}, [setIsOpen]);
return [isOpen, setModalOpen, setModalClosed];
}

View File

@ -857,7 +857,7 @@ function InteractiveImportModalContent(
<MenuContent>
<SelectedMenuItem
name={'all'}
name="all"
isSelected={!filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
@ -865,7 +865,7 @@ function InteractiveImportModalContent(
</SelectedMenuItem>
<SelectedMenuItem
name={'new'}
name="new"
isSelected={filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
@ -945,7 +945,7 @@ function InteractiveImportModalContent(
<SelectInput
className={styles.bulkSelect}
name="select"
value={'select'}
value="select"
values={bulkSelectOptions}
isDisabled={!selectedIds.length}
onChange={onSelectModalSelect}

View File

@ -17,7 +17,7 @@ function SelectLanguageModal(props: SelectLanguageModalProps) {
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<SelectLanguageModalContent
languageIds={languageIds}
modalTitle={modalTitle}

View File

@ -64,19 +64,20 @@ interface RowItemData {
onSeriesSelect(seriesId: number): void;
}
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
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;
}
const series = items[index];
return (
<VirtualTableRowButton
style={{
@ -84,7 +85,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
justifyContent: 'space-between',
...style,
}}
onPress={() => onSeriesSelect(series.id)}
onPress={handlePress}
>
<SelectSeriesRow
key={series.id}
@ -98,7 +99,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
/>
</VirtualTableRowButton>
);
};
}
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
const { modalTitle, onSeriesSelect, onModalClose } = props;
@ -197,9 +198,9 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
/>
<Scroller
ref={scrollerRef}
className={styles.scroller}
autoFocus={false}
ref={scrollerRef}
>
<SelectSeriesModalTableHeader columns={columns} />
<List<RowItemData>

View File

@ -17,7 +17,7 @@ function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<SelectDownloadClientModalContent
protocol={protocol}
modalTitle={modalTitle}

View File

@ -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 { icons } from 'Helpers/Props';
import ParseModal from 'Parse/ParseModal';
@ -16,7 +16,7 @@ function ParseToolbarButton() {
}, [setIsParseModalOpen]);
return (
<Fragment>
<>
<PageToolbarButton
label={translate('TestParsing')}
iconName={icons.PARSE}
@ -24,7 +24,7 @@ function ParseToolbarButton() {
/>
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
</Fragment>
</>
);
}

View File

@ -21,7 +21,7 @@ interface RootFolderRowProps {
}
function RootFolderRow(props: RootFolderRowProps) {
const { id, path, accessible, freeSpace, unmappedFolders = [] } = props;
const { id, path, accessible, freeSpace = 0, unmappedFolders = [] } = props;
const isUnavailable = !accessible;

View File

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@ -6,14 +5,19 @@ import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './SeasonInfo.css';
function SeasonInfo(props) {
const {
totalEpisodeCount,
monitoredEpisodeCount,
episodeFileCount,
sizeOnDisk
} = props;
interface SeasonInfoProps {
totalEpisodeCount: number;
monitoredEpisodeCount: number;
episodeFileCount: number;
sizeOnDisk: number;
}
function SeasonInfo({
totalEpisodeCount,
monitoredEpisodeCount,
episodeFileCount,
sizeOnDisk,
}: SeasonInfoProps) {
return (
<DescriptionList>
<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;

View File

@ -212,8 +212,8 @@ class SeriesDetails extends Component {
} = this.props;
const {
episodeFileCount,
sizeOnDisk
episodeFileCount = 0,
sizeOnDisk = 0
} = statistics;
const {
@ -454,10 +454,9 @@ class SeriesDetails extends Component {
name={icons.DRIVE}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk || 0)
}
{formatBytes(sizeOnDisk)}
</span>
</div>
</Label>

View File

@ -194,10 +194,12 @@ function getInfoRowProps(
}
if (name === 'sizeOnDisk') {
const { sizeOnDisk = 0 } = props;
return {
title: translate('SizeOnDisk'),
iconName: icons.DRIVE,
label: formatBytes(props.sizeOnDisk),
label: formatBytes(sizeOnDisk),
};
}

View File

@ -42,11 +42,7 @@ interface SeriesIndexOverviewsProps {
isSmallScreen: boolean;
}
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, ...otherData } = data;
if (index >= items.length) {
@ -60,7 +56,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
<SeriesIndexOverview seriesId={series.id} {...otherData} />
</div>
);
};
}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;

View File

@ -37,7 +37,7 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
added,
seasonCount,
path,
sizeOnDisk,
sizeOnDisk = 0,
tags,
sortKey,
showRelativeDates,

View File

@ -60,12 +60,12 @@ const seriesIndexSelector = createSelector(
}
);
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
function Cell({
columnIndex,
rowIndex,
style,
data,
}) => {
}: GridChildComponentProps<CellItemData>) {
const { layout, items, sortKey, isSelectMode } = data;
const { columnCount, padding, posterWidth, posterHeight } = layout;
const index = rowIndex * columnCount + columnIndex;
@ -92,7 +92,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
/>
</div>
);
};
}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;

View File

@ -45,11 +45,7 @@ const columnsSelector = createSelector(
(columns) => columns
);
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, sortKey, columns, isSelectMode } = data;
if (index >= items.length) {
@ -75,7 +71,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
/>
</div>
);
};
}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;

View File

@ -1,4 +1,4 @@
import React, { Fragment, useCallback } from 'react';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@ -32,7 +32,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
);
return (
<Fragment>
<>
<FormGroup>
<FormLabel>{translate('ShowBanners')}</FormLabel>
@ -56,7 +56,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
onChange={onTableOptionChangeWrapper}
/>
</FormGroup>
</Fragment>
</>
);
}

View File

@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent';
@ -17,11 +17,11 @@ function CustomFormatSettingsPage() {
// @ts-ignore
showSave={false}
additionalButtons={
<Fragment>
<>
<PageToolbarSeparator />
<ParseToolbarButton />
</Fragment>
</>
}
/>

View File

@ -231,9 +231,9 @@ function ManageDownloadClientsModalContent(
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSelectAllChange={onSelectAllChange}
onSortPress={onSortPress}
>
<TableBody>
@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
<ManageDownloadClientsEditModal
isOpen={isEditModalOpen}
downloadClientIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
downloadClientIds={selectedIds}
/>
<TagsModal

View File

@ -32,7 +32,7 @@ function EditImportListExclusionModal(
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
<EditImportListExclusionModalContent
{...otherProps}
onModalClose={onModalClose}
onModalClose={onModalClosePress}
/>
</Modal>
);

View File

@ -261,9 +261,9 @@ function ManageImportListsModalContent(
<ManageImportListsEditModal
isOpen={isEditModalOpen}
importListIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
importListIds={selectedIds}
/>
<TagsModal

View File

@ -226,9 +226,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSelectAllChange={onSelectAllChange}
onSortPress={onSortPress}
>
<TableBody>
@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
<ManageIndexersEditModal
isOpen={isEditModalOpen}
indexerIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
indexerIds={selectedIds}
/>
<TagsModal

View File

@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
return function(getState, payload, dispatch) {
const {
id,
...queryParams
queryParams
} = payload;
dispatch(set({ section, isDeleting: true }));

View File

@ -251,6 +251,11 @@ export const filterBuilderProps = [
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.SERIES_TYPES
},
{
name: 'title',
label: () => translate('Title'),
type: filterBuilderTypes.STRING
},
{
name: 'network',
label: () => translate('Network'),

View File

@ -66,7 +66,7 @@ function About() {
) : null}
{isDocker ? (
<DescriptionListItem title={translate('Docker')} data={'Yes'} />
<DescriptionListItem title={translate('Docker')} data="Yes" />
) : null}
<DescriptionListItem

View File

@ -1,10 +1,4 @@
import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
@ -158,7 +152,7 @@ function Updates() {
{translate('InstallLatest')}
</SpinnerButton>
) : (
<Fragment>
<>
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
<div className={styles.message}>
@ -171,7 +165,7 @@ function Updates() {
}
/>
</div>
</Fragment>
</>
)}
{isFetching ? (

View File

@ -1,10 +1,6 @@
import { filesize } from 'filesize';
function formatBytes(input?: string | number) {
if (!input) {
return '';
}
function formatBytes(input: string | number) {
const size = Number(input);
if (isNaN(size)) {

View File

@ -1,11 +1,11 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Test.ExtensionTests
{
[TestFixture]
public class Int64ExtensionFixture
public class NumberExtensionFixture
{
[TestCase(0, "0 B")]
[TestCase(1000, "1,000.0 B")]

View File

@ -380,8 +380,17 @@ namespace NzbDrone.Common.Test
[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();
}
}

View File

@ -1,9 +1,9 @@
using System;
using System;
using System.Globalization;
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" };
@ -26,5 +26,25 @@ namespace NzbDrone.Common.Extensions
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);
}
}
}

View File

@ -152,16 +152,20 @@ namespace NzbDrone.Common.Extensions
return false;
}
var directoryInfo = new DirectoryInfo(path);
while (directoryInfo != null)
// Only check for leading or trailing spaces for path when running on Windows.
if (OsInfo.IsWindows)
{
if (directoryInfo.Name.Trim() != directoryInfo.Name)
{
return false;
}
var directoryInfo = new DirectoryInfo(path);
directoryInfo = directoryInfo.Parent;
while (directoryInfo != null)
{
if (directoryInfo.Name.Trim() != directoryInfo.Name)
{
return false;
}
directoryInfo = directoryInfo.Parent;
}
}
if (validationType == PathValidationType.AnyOs)

View File

@ -1,6 +1,6 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NLog.Fluent;
namespace NzbDrone.Common.Instrumentation.Extensions
{
@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{
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);
}
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);
}
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);
}
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);
}
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);
}
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
{
SentryLogger.Log(level)
.CopyLogEvent(logBuilder.LogEventInfo)
SentryLogger.ForLogEvent(level)
.CopyLogEvent(logBuilder.LogEvent)
.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)
.TimeStamp(logEvent.TimeStamp)
return logBuilder.TimeStamp(logEvent.TimeStamp)
.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);
}
}

View File

@ -1,13 +1,15 @@
using NLog;
using System.Text;
using NLog;
using NLog.Targets;
namespace NzbDrone.Common.Instrumentation
{
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);
}
}
}

View File

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO;
using NLog;
using NLog.Config;
using NLog.Layouts.ClefJsonLayout;
using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@ -13,6 +14,8 @@ namespace NzbDrone.Common.Instrumentation
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}}";
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;
@ -34,6 +37,8 @@ namespace NzbDrone.Common.Instrumentation
var appFolderInfo = new AppFolderInfo(startupContext);
RegisterGlobalFilters();
if (Debugger.IsAttached)
{
RegisterDebugger();
@ -122,7 +127,16 @@ namespace NzbDrone.Common.Instrumentation
var coloredConsoleTarget = new ColoredConsoleTarget();
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);
@ -148,7 +162,7 @@ namespace NzbDrone.Common.Instrumentation
fileTarget.ConcurrentWrites = false;
fileTarget.ConcurrentWriteAttemptDelay = 50;
fileTarget.ConcurrentWriteAttempts = 10;
fileTarget.ArchiveAboveSize = 1024000;
fileTarget.ArchiveAboveSize = 1.Megabytes();
fileTarget.MaxArchiveFiles = maxArchiveFiles;
fileTarget.EnableFileDelete = true;
fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling;
@ -196,6 +210,17 @@ namespace NzbDrone.Common.Instrumentation
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)
{
return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", ""));
@ -206,4 +231,10 @@ namespace NzbDrone.Common.Instrumentation
return GetLogger(obj.GetType());
}
}
public enum ConsoleLogFormat
{
Standard,
Clef
}
}

View File

@ -5,8 +5,10 @@ public class LogOptions
public string Level { get; set; }
public bool? FilterSentryEvents { get; set; }
public int? Rotate { get; set; }
public int? SizeLimit { get; set; }
public bool? Sql { get; set; }
public string ConsoleLevel { get; set; }
public string ConsoleFormat { get; set; }
public bool? AnalyticsEnabled { get; set; }
public string SyslogServer { get; set; }
public int? SyslogPort { get; set; }

View File

@ -8,9 +8,10 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="NLog" Version="5.3.2" />
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" />
<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="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Text.Json" Version="6.0.9" />

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;

View File

@ -3,6 +3,7 @@ using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.MediaFiles.MediaInfo;

View File

@ -4,6 +4,7 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;

View File

@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
};
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);
}
@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
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());
_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();
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());
_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();
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());
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null), Times.Never());
@ -212,7 +212,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
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>();
_reports = new List<ReleaseInfo>
@ -224,7 +224,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
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);
}
@ -263,8 +263,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
}).ToList();
Mocker.GetMock<IParsingService>()
.Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
.Returns<ParsedEpisodeInfo, int, int, SearchCriteriaBase>((p, tvdbid, tvrageid, c) =>
.Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SearchCriteriaBase>()))
.Returns<ParsedEpisodeInfo, int, int, string, SearchCriteriaBase>((p, _, _, _, _) =>
new RemoteEpisode
{
DownloadAllowed = true,
@ -318,7 +318,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
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>();
_reports = new List<ReleaseInfo>

View File

@ -4,6 +4,8 @@ using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download.Aggregation.Aggregators;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.TorrentRss;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;
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);
}
[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]
public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title()
{

View File

@ -10,7 +10,6 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
@ -35,7 +34,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
.Returns(30);
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());
Mocker.GetMock<IHttpClient>()

View File

@ -49,10 +49,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[Test]
public void magnet_download_should_not_return_the_item()
public void magnet_download_should_be_returned_as_queued()
{
PrepareClientToReturnMagnetItem();
Subject.GetItems().Count().Should().Be(0);
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Queued);
}
[Test]

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
@ -60,7 +60,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
public void magnet_download_should_not_return_the_item()
{
PrepareClientToReturnMagnetItem();
Subject.GetItems().Count().Should().Be(0);
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Queued);
}
[Test]

View File

@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
.Returns(remoteEpisode);
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);
var client = new DownloadClientDefinition()
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
};
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);
Mocker.GetMock<IHistoryService>()
@ -199,7 +199,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
Subject.GetTrackedDownloads().Should().HaveCount(1);
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));
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>()
.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));
Mocker.GetMock<IHistoryService>()
@ -258,7 +258,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
Subject.GetTrackedDownloads().Should().HaveCount(1);
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));
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>()
.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));
Mocker.GetMock<IHistoryService>()
@ -317,7 +317,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
Subject.GetTrackedDownloads().Should().HaveCount(1);
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));
Subject.Handle(new SeriesDeletedEvent(new List<Series> { remoteEpisode.Series }, true, true));

View File

@ -15,6 +15,10 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
[TestCase("Franklin & Bash", "Franklin+and+Bash")]
[TestCase("Chicago P.D.", "Chicago+PD")]
[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 Whites Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
[TestCase("Betty Whites Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
public void should_replace_some_special_characters(string input, string expected)
{
Subject.SceneTitles = new List<string> { input };

View File

@ -7,6 +7,7 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.MediaFiles;

View File

@ -6,6 +6,7 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
@ -74,8 +75,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
.Returns(new List<EpisodeHistory>());
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
.With(d => d.OutputPath = new OsPath(outputPath))
.Build();
.With(d => d.OutputPath = new OsPath(outputPath))
.Build();
}
private void GivenNewDownload()

View File

@ -1,10 +1,11 @@
using System.IO;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser.Model;

View File

@ -233,11 +233,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenEpisodes(actualInfo, actualInfo.EpisodeNumbers);
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);
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);
Subject.IsSatisfiedBy(localEpisode, null).Accepted.Should().BeTrue();

View File

@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
GivenDailySeries();
GivenDailyParseResult();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
@ -103,7 +103,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
GivenDailySeries();
GivenDailyParseResult();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
GivenDailySeries();
_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>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Once());
@ -128,7 +128,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
GivenDailyParseResult();
_parsedEpisodeInfo.DailyPart = 1;
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<IEpisodeService>()
.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>()))
.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>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), It.IsAny<string>(), null), Times.Never());
@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenSceneNumberingSeries();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<IEpisodeService>()
.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();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
Mocker.GetMock<IEpisodeService>()
.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();
_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>()
.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]
public void should_find_episode()
{
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<IEpisodeService>()
.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]
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>()
.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;
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
Mocker.GetMock<IEpisodeService>()
.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)
.Build());
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<IEpisodeService>()
.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))
.Returns((Episode)null);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(_series.TvdbId, _parsedEpisodeInfo.SeasonNumber, _parsedEpisodeInfo.EpisodeNumbers.First()), Times.Once());

View File

@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenMatchBySeriesTitle();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Once());
@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenMatchByTvdbId();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenMatchByTvRageId();
Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId);
Subject.Map(_parsedEpisodeInfo, 0, _series.TvRageId, null);
Mocker.GetMock<ISeriesService>()
.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>()))
.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>()
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Never());
@ -136,7 +136,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenMatchBySeriesTitle();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
@ -147,7 +147,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenParseResultSeriesDoesntMatchSearchCriteria();
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.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))
.Returns(_series);
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>()), Times.Once());
@ -180,7 +180,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenParseResultSeriesDoesntMatchSearchCriteria();
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
@ -191,7 +191,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenParseResultSeriesDoesntMatchSearchCriteria();
Subject.Map(_parsedEpisodeInfo, 0, 10, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, 0, 10, null, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTvRageId(It.IsAny<int>()), Times.Once());
@ -202,12 +202,34 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenParseResultSeriesDoesntMatchSearchCriteria();
Subject.Map(_parsedEpisodeInfo, 10, 10, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, 10, 10, null, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.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]
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>()))
.Returns(_series.TvdbId);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());
@ -226,7 +248,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenParseResultSeriesDoesntMatchSearchCriteria();
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, _singleEpisodeSearchCriteria);
Mocker.GetMock<ISeriesService>()
.Verify(v => v.FindByTitle(It.IsAny<string>()), Times.Never());

View File

@ -185,8 +185,10 @@ namespace NzbDrone.Core.Blocklisting
Indexer = message.Data.GetValueOrDefault("indexer"),
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
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))

View File

@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Options;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events;
@ -38,8 +39,10 @@ namespace NzbDrone.Core.Configuration
bool AnalyticsEnabled { get; }
string LogLevel { get; }
string ConsoleLogLevel { get; }
ConsoleLogFormat ConsoleLogFormat { get; }
bool LogSql { get; }
int LogRotate { get; }
int LogSizeLimit { get; }
bool FilterSentryEvents { get; }
string Branch { get; }
string ApiKey { get; }
@ -220,9 +223,14 @@ namespace NzbDrone.Core.Configuration
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 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 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 LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, 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 string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");

View File

@ -1,4 +1,5 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;

View File

@ -63,7 +63,9 @@ namespace NzbDrone.Core.DataAugmentation.Scene
sceneSeasonNumbers.Contains(n.SceneSeasonNumber ?? -1) ||
((n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1 && n.SceneOrigin != "tvdb"))
.Where(n => IsEnglish(n.SearchTerm))
.Select(n => n.SearchTerm).Distinct().ToList();
.Select(n => n.SearchTerm)
.Distinct(StringComparer.InvariantCultureIgnoreCase)
.ToList();
return names;
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;

View File

@ -80,7 +80,7 @@ namespace NzbDrone.Core.DecisionEngine
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)
{
@ -90,7 +90,7 @@ namespace NzbDrone.Core.DecisionEngine
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;
if (remoteEpisode.Series == null)

View File

@ -1,4 +1,5 @@
using NLog;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
@ -10,10 +12,13 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators
{
public class AggregateLanguages : IAggregateRemoteEpisode
{
private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger;
public AggregateLanguages(Logger logger)
public AggregateLanguages(IIndexerFactory indexerFactory,
Logger logger)
{
_indexerFactory = indexerFactory;
_logger = logger;
}
@ -71,6 +76,17 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators
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
if (languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown))
{

View File

@ -129,10 +129,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2
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,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = torrent.InfoHash?.ToUpper(),
@ -146,7 +144,12 @@ namespace NzbDrone.Core.Download.Clients.Aria2
Status = status,
Title = title,
TotalSize = totalLength,
CanMoveFiles = false
};
queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == "complete";
yield return queueItem;
}
}

View File

@ -89,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{
foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod))
{
yield return new DownloadClientItem
var queueItem = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = Definition.Name + "_" + item.DownloadId,
@ -101,11 +101,14 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
OutputPath = item.OutputPath,
Status = item.Status,
CanMoveFiles = !Settings.ReadOnly,
CanBeRemoved = !Settings.ReadOnly
Status = item.Status
};
queueItem.CanMoveFiles = queueItem.CanBeRemoved =
queueItem.DownloadClientInfo.RemoveCompletedDownloads &&
!Settings.ReadOnly;
yield return queueItem;
}
}

View File

@ -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.
// This allows Sonarr to delete the torrent as appropriate.
item.CanMoveFiles = item.CanBeRemoved =
item.DownloadClientInfo.RemoveCompletedDownloads &&
torrent.IsAutoManaged &&
torrent.StopAtRatio &&
torrent.Ratio >= torrent.StopRatio &&
@ -200,7 +201,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
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;

View File

@ -88,7 +88,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
}
}
var item = new DownloadClientItem()
var item = new DownloadClientItem
{
Category = Settings.TvCategory,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
@ -99,11 +99,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
RemainingTime = GetRemainingTime(torrent),
SeedRatio = GetSeedRatio(torrent),
Status = GetStatus(torrent),
Message = GetMessage(torrent),
CanMoveFiles = IsFinished(torrent),
CanBeRemoved = IsFinished(torrent)
Message = GetMessage(torrent)
};
item.CanMoveFiles = item.CanBeRemoved = item.DownloadClientInfo.RemoveCompletedDownloads && IsFinished(torrent);
if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed)
{
item.OutputPath = GetOutputPath(outputPath, torrent, serialNumber);

View File

@ -153,7 +153,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
item.Status = DownloadItemStatus.Downloading;
}
if (item.Status == DownloadItemStatus.Completed)
if (item.DownloadClientInfo.RemoveCompletedDownloads && item.Status == DownloadItemStatus.Completed)
{
// Grab cached seedConfig
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(item.DownloadId);
@ -165,7 +165,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
// Check if seed ratio reached
item.CanMoveFiles = item.CanBeRemoved = true;
}
else if (properties.DateFinished != null && properties.DateFinished > 0)
else if (properties.DateFinished is > 0)
{
// Check if seed time reached
if ((DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds((long)properties.DateFinished)) >= seedConfig.SeedTime)

View File

@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
break;
}
item.CanBeRemoved = item.CanMoveFiles = torrent.Status == FreeboxDownloadTaskStatus.Done;
item.CanBeRemoved = item.CanMoveFiles = item.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == FreeboxDownloadTaskStatus.Done;
queueItems.Add(item);
}

View File

@ -92,7 +92,10 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
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);
}

View File

@ -225,7 +225,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
foreach (var torrent in torrents)
{
var item = new DownloadClientItem()
var item = new DownloadClientItem
{
DownloadId = torrent.Hash.ToUpper(),
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.
// 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)
{

View File

@ -43,12 +43,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
foreach (var torrent in torrents)
{
// If totalsize == 0 the torrent is a magnet downloading metadata
if (torrent.TotalSize == 0)
{
continue;
}
var outputPath = new OsPath(torrent.DownloadDir);
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
@ -99,6 +93,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
item.Status = DownloadItemStatus.Warning;
item.Message = torrent.ErrorString;
}
else if (torrent.TotalSize == 0)
{
item.Status = DownloadItemStatus.Queued;
}
else if (torrent.LeftUntilDone == 0 && (torrent.Status == TransmissionTorrentStatus.Stopped ||
torrent.Status == TransmissionTorrentStatus.Seeding ||
torrent.Status == TransmissionTorrentStatus.SeedingWait))
@ -119,7 +117,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
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;
items.Add(item);

View File

@ -185,7 +185,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
// Grab cached seedConfig
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(torrent.Hash);
if (torrent.IsFinished && seedConfig != null)
if (item.DownloadClientInfo.RemoveCompletedDownloads && torrent.IsFinished && seedConfig != null)
{
var canRemove = false;

View File

@ -167,6 +167,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
// 'Started' without 'Queued' is when the torrent is 'forced seeding'
item.CanMoveFiles = item.CanBeRemoved =
item.DownloadClientInfo.RemoveCompletedDownloads &&
!torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) &&
!torrent.Status.HasFlag(UTorrentTorrentStatus.Started);

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
@ -247,14 +246,14 @@ namespace NzbDrone.Core.Download
}
else
{
_logger.Debug()
_logger.ForDebugEvent()
.Message("No Episodes were just imported, but all episodes were previously imported, possible issue with download history.")
.Property("SeriesId", trackedDownload.RemoteEpisode.Series.Id)
.Property("DownloadId", trackedDownload.DownloadItem.DownloadId)
.Property("Title", trackedDownload.DownloadItem.Title)
.Property("Path", trackedDownload.ImportItem.OutputPath.ToString())
.WriteSentryWarn("DownloadHistoryIncomplete")
.Write();
.Log();
}
var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id));

View File

@ -37,6 +37,7 @@ namespace NzbDrone.Core.Download
public string Type { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool HasPostImportCategory { get; set; }
public static DownloadClientItemClientInfo FromDownloadClient<TSettings>(
@ -49,6 +50,7 @@ namespace NzbDrone.Core.Download
Type = downloadClient.Name,
Id = downloadClient.Definition.Id,
Name = downloadClient.Definition.Name,
RemoveCompletedDownloads = downloadClient.Definition is DownloadClientDefinition { RemoveCompletedDownloads: true },
HasPostImportCategory = hasPostImportCategory
};
}

View File

@ -12,7 +12,7 @@ namespace NzbDrone.Core.Download
public interface IFailedDownloadService
{
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 ProcessFailed(TrackedDownload trackedDownload);
}
@ -20,7 +20,6 @@ namespace NzbDrone.Core.Download
public class FailedDownloadService : IFailedDownloadService
{
private readonly IHistoryService _historyService;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IEventAggregator _eventAggregator;
public FailedDownloadService(IHistoryService historyService,
@ -28,7 +27,6 @@ namespace NzbDrone.Core.Download
IEventAggregator eventAggregator)
{
_historyService = historyService;
_trackedDownloadService = trackedDownloadService;
_eventAggregator = eventAggregator;
}
@ -37,9 +35,10 @@ namespace NzbDrone.Core.Download
var history = _historyService.Get(historyId);
var downloadId = history.DownloadId;
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;
}
@ -53,21 +52,19 @@ namespace NzbDrone.Core.Download
}
// 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();
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())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
PublishDownloadFailedEvent(history, "Manually marked as failed", trackedDownload, skipRedownload: skipRedownload);
PublishDownloadFailedEvent(history.First(), GetEpisodeIds(history), "Manually marked as failed", trackedDownload, skipRedownload: skipRedownload);
}
}
@ -82,9 +79,7 @@ namespace NzbDrone.Core.Download
if (trackedDownload.DownloadItem.IsEncrypted ||
trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
{
var grabbedItems = _historyService
.Find(trackedDownload.DownloadItem.DownloadId, EpisodeHistoryEventType.Grabbed)
.ToList();
var grabbedItems = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId);
if (grabbedItems.Empty())
{
@ -103,9 +98,7 @@ namespace NzbDrone.Core.Download
return;
}
var grabbedItems = _historyService
.Find(trackedDownload.DownloadItem.DownloadId, EpisodeHistoryEventType.Grabbed)
.ToList();
var grabbedItems = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId);
if (grabbedItems.Empty())
{
@ -124,18 +117,17 @@ namespace NzbDrone.Core.Download
}
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);
var downloadFailedEvent = new DownloadFailedEvent
{
SeriesId = historyItem.SeriesId,
EpisodeIds = historyItems.Select(h => h.EpisodeId).Distinct().ToList(),
EpisodeIds = episodeIds,
Quality = historyItem.Quality,
SourceTitle = historyItem.SourceTitle,
DownloadClient = historyItem.Data.GetValueOrDefault(EpisodeHistory.DOWNLOAD_CLIENT),
@ -145,10 +137,23 @@ namespace NzbDrone.Core.Download
TrackedDownload = trackedDownload,
Languages = historyItem.Languages,
SkipRedownload = skipRedownload,
ReleaseSource = releaseSource
ReleaseSource = releaseSource,
};
_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();
}
}
}

View File

@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
if (parsedEpisodeInfo != null)
{
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0);
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0, null);
_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
// 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) ??
_parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, firstHistoryItem.SourceTitle, 0, 0);
_parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, firstHistoryItem.SourceTitle, 0, 0, null);
if (parsedEpisodeInfo != null)
{
@ -234,7 +234,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
{
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);
}

View File

@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{

View File

@ -20,26 +20,6 @@ namespace NzbDrone.Core
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)
{
return Convert.ToInt64(Math.Floor((decimal)number / level) * level);

View File

@ -165,6 +165,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Guid", message.Episode.Release.Guid);
history.Data.Add("TvdbId", message.Episode.Release.TvdbId.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("CustomFormatScore", message.Episode.CustomFormatScore.ToString());
history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString());

View File

@ -10,7 +10,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
{
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 BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);

View File

@ -193,7 +193,7 @@ namespace NzbDrone.Core.IndexerSearch
foreach (var item in dict)
{
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();
@ -221,7 +221,7 @@ namespace NzbDrone.Core.IndexerSearch
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();
@ -463,7 +463,7 @@ namespace NzbDrone.Core.IndexerSearch
spec.UserInvokedSearch = userInvokedSearch;
spec.InteractiveSearch = interactiveSearch;
if (!spec.SceneTitles.Contains(series.Title))
if (!spec.SceneTitles.Contains(series.Title, StringComparer.InvariantCultureIgnoreCase))
{
spec.SceneTitles.Add(series.Title);
}

View File

@ -95,6 +95,11 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
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);
}

View File

@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList
{
var id = result.Id;
torrentInfos.Add(new TorrentInfo
var torrentInfo = new TorrentInfo
{
Guid = $"FileList-{id}",
Title = result.Name,
@ -48,9 +48,15 @@ namespace NzbDrone.Core.Indexers.FileList
Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders,
PublishDate = result.UploadDate.ToUniversalTime(),
ImdbId = result.ImdbId,
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();

View File

@ -61,6 +61,7 @@ namespace NzbDrone.Core.Indexers.HDBits
Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders,
PublishDate = result.Added.ToUniversalTime(),
TvdbId = result.TvdbInfo?.Id ?? 0,
IndexerFlags = GetIndexerFlags(result)
});
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
@ -20,8 +19,6 @@ namespace NzbDrone.Core.Indexers
public abstract class IndexerBase<TSettings> : IIndexer
where TSettings : IIndexerSettings, new()
{
private static readonly Regex MultiRegex = new (@"[_. ](?<multi>multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
protected readonly IIndexerStatusService _indexerStatusService;
protected readonly IConfigService _configService;
protected readonly IParsingService _parsingService;
@ -94,7 +91,7 @@ namespace NzbDrone.Core.Indexers
result.ForEach(c =>
{
// 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();
}

View File

@ -90,6 +90,7 @@ namespace NzbDrone.Core.Indexers.Newznab
releaseInfo.TvdbId = GetTvdbId(item);
releaseInfo.TvRageId = GetTvRageId(item);
releaseInfo.ImdbId = GetImdbId(item);
return releaseInfo;
}
@ -182,6 +183,18 @@ namespace NzbDrone.Core.Indexers.Newznab
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 = "")
{
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));

View File

@ -83,6 +83,7 @@ namespace NzbDrone.Core.Indexers.Torznab
{
torrentInfo.TvdbId = GetTvdbId(item);
torrentInfo.TvRageId = GetTvRageId(item);
releaseInfo.ImdbId = GetImdbId(item);
torrentInfo.IndexerFlags = GetFlags(item);
}
@ -177,6 +178,18 @@ namespace NzbDrone.Core.Indexers.Torznab
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)
{
return TryGetTorznabAttribute(item, "infohash");

View File

@ -33,22 +33,25 @@ namespace NzbDrone.Core.Instrumentation
LogManager.Configuration.AddTarget("DbLogger", target);
LogManager.Configuration.LoggingRules.Add(Rule);
LogManager.ConfigurationReloaded += OnLogManagerOnConfigurationReloaded;
LogManager.ConfigurationChanged += OnLogManagerOnConfigurationReloaded;
LogManager.ReconfigExistingLoggers();
}
public void UnRegister()
{
LogManager.ConfigurationReloaded -= OnLogManagerOnConfigurationReloaded;
LogManager.ConfigurationChanged -= OnLogManagerOnConfigurationReloaded;
LogManager.Configuration.RemoveTarget("DbLogger");
LogManager.Configuration.LoggingRules.Remove(Rule);
LogManager.ReconfigExistingLoggers();
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; }

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using NLog;
using NLog.Config;
using NLog.Targets;
using NLog.Targets.Syslog;
using NLog.Targets.Syslog.Settings;
using NzbDrone.Common.EnvironmentInfo;
@ -51,13 +52,14 @@ namespace NzbDrone.Core.Instrumentation
var rules = LogManager.Configuration.LoggingRules;
// Console
ReconfigureConsole();
SetMinimumLogLevel(rules, "consoleLogger", minimumConsoleLogLevel);
// Log Files
SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off);
SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off);
SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off);
SetLogRotation();
ReconfigureFile();
// Log Sql
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>())
{
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)
{
var syslogTarget = new SyslogTarget();
@ -117,7 +136,7 @@ namespace NzbDrone.Core.Instrumentation
syslogTarget.MessageSend.Protocol = ProtocolType.Udp;
syslogTarget.MessageSend.Udp.Port = syslogPort;
syslogTarget.MessageSend.Udp.Server = syslogServer;
syslogTarget.MessageSend.Udp.ReconnectInterval = 500;
syslogTarget.MessageSend.Retry.ConstantBackoff.BaseDelay = 500;
syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424;
syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName;

View File

@ -1,4 +1,6 @@
{
"AddAutoTag": "أضف كلمات دلالية تلقائيا",
"AddCondition": "إضافة شرط"
"AddCondition": "إضافة شرط",
"AutoTaggingNegateHelpText": "إذا تم تحديده ، فلن يتم تطبيق التنسيق المخصص إذا تطابق شرط {implementationName} هذا.",
"ConnectionLostReconnect": "سيحاول {appName} الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه."
}

View File

@ -1325,8 +1325,8 @@
"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",
"NotificationsEmbySettingsSendNotifications": "Send Notifications",
"NotificationsEmbySettingsSendNotificationsHelpText": "Have MediaBrowser send notifications to configured providers",
"NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete?",
"NotificationsEmbySettingsSendNotificationsHelpText": "Have Emby send notifications to configured providers. Not supported on Jellyfin.",
"NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete",
"NotificationsGotifySettingIncludeSeriesPoster": "Include Series Poster",
"NotificationsGotifySettingIncludeSeriesPosterHelpText": "Include series poster in message",
"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",
"NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title",
"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",
"NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound",
"NotificationsTelegramSettingsTopicId": "Topic ID",

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