mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
14 Commits
v4.0.8.196
...
v4.0.8.200
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1527f9abb | ||
|
|
291d792810 | ||
|
|
9b528eb829 | ||
|
|
4c0b896174 | ||
|
|
4ff83f9efc | ||
|
|
217611d716 | ||
|
|
1299a97579 | ||
|
|
4c0de55672 | ||
|
|
78a0def46a | ||
|
|
11a9dcb389 | ||
|
|
4eab168267 | ||
|
|
c9b5a1258a | ||
|
|
9127a91dfc | ||
|
|
cc85a28ff7 |
@@ -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'
|
||||
})
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -261,9 +261,9 @@ function ManageImportListsModalContent(
|
||||
|
||||
<ManageImportListsEditModal
|
||||
isOpen={isEditModalOpen}
|
||||
importListIds={selectedIds}
|
||||
onModalClose={onEditModalClose}
|
||||
onSavePress={onSavePress}
|
||||
importListIds={selectedIds}
|
||||
/>
|
||||
|
||||
<TagsModal
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -66,7 +66,7 @@ function About() {
|
||||
) : null}
|
||||
|
||||
{isDocker ? (
|
||||
<DescriptionListItem title={translate('Docker')} data={'Yes'} />
|
||||
<DescriptionListItem title={translate('Docker')} data="Yes" />
|
||||
) : null}
|
||||
|
||||
<DescriptionListItem
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -74,8 +74,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()
|
||||
|
||||
@@ -220,7 +220,7 @@ 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 string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"AddAutoTag": "أضف كلمات دلالية تلقائيا",
|
||||
"AddCondition": "إضافة شرط"
|
||||
"AddCondition": "إضافة شرط",
|
||||
"AutoTaggingNegateHelpText": "إذا تم تحديده ، فلن يتم تطبيق التنسيق المخصص إذا تطابق شرط {implementationName} هذا.",
|
||||
"ConnectionLostReconnect": "سيحاول {appName} الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1839,7 +1839,7 @@
|
||||
"Titles": "Títulos",
|
||||
"ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar",
|
||||
"TotalFileSize": "Tamaño total de archivo",
|
||||
"UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización",
|
||||
"UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización: {version}",
|
||||
"UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado",
|
||||
"UrlBase": "URL base",
|
||||
"UseSsl": "Usar SSL",
|
||||
@@ -2093,5 +2093,12 @@
|
||||
"CountVotes": "{votes} votos",
|
||||
"InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?",
|
||||
"InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información.",
|
||||
"NextAiringDate": "Siguiente emisión: {date}"
|
||||
"NextAiringDate": "Siguiente emisión: {date}",
|
||||
"SeasonsMonitoredAll": "Todas",
|
||||
"SeasonsMonitoredNone": "Ninguna",
|
||||
"SeasonsMonitoredStatus": "Temporadas monitorizadas",
|
||||
"NoBlocklistItems": "Ningún elemento en la lista de bloqueo",
|
||||
"SeasonsMonitoredPartial": "Parcial",
|
||||
"NotificationsTelegramSettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones",
|
||||
"NotificationsTelegramSettingsMetadataLinks": "Enlaces de metadatos"
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@
|
||||
"RootFolderSelectFreeSpace": "{freeSpace} Libre",
|
||||
"WantMoreControlAddACustomFormat": "Vous souhaitez avoir plus de contrôle sur les téléchargements préférés ? Ajoutez un [Format personnalisé](/settings/customformats)",
|
||||
"RemoveSelectedItemsQueueMessageText": "Voulez-vous vraiment supprimer {selectedCount} éléments de la file d'attente ?",
|
||||
"UpdateAll": "Tout actualiser",
|
||||
"UpdateAll": "Actualiser",
|
||||
"EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif",
|
||||
"UnmonitorDeletedEpisodesHelpText": "Les épisodes effacés du disque dur ne seront plus surveillés dans {appName}",
|
||||
"RssSync": "Synchronisation RSS",
|
||||
@@ -2076,5 +2076,7 @@
|
||||
"UnableToImportAutomatically": "Impossible d'importer automatiquement",
|
||||
"DayOfWeekAt": "{day} à {time}",
|
||||
"TomorrowAt": "Demain à {time}",
|
||||
"TodayAt": "Aujourd'hui à {time}"
|
||||
"TodayAt": "Aujourd'hui à {time}",
|
||||
"ShowTagsHelpText": "Afficher les labels sous l'affiche",
|
||||
"ShowTags": "Afficher les labels"
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"SizeOnDisk": "Tamanho no disco",
|
||||
"SystemTimeHealthCheckMessage": "A hora do sistema está desligada por mais de 1 dia. Tarefas agendadas podem não ser executadas corretamente até que o horário seja corrigido",
|
||||
"Unmonitored": "Não monitorado",
|
||||
"UpdateAvailableHealthCheckMessage": "Nova atualização está disponível",
|
||||
"UpdateAvailableHealthCheckMessage": "Nova atualização disponível: {version}",
|
||||
"Added": "Adicionado",
|
||||
"ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração",
|
||||
"RemoveCompletedDownloads": "Remover downloads concluídos",
|
||||
@@ -2093,5 +2093,12 @@
|
||||
"Install": "Instalar",
|
||||
"InstallMajorVersionUpdate": "Instalar Atualização",
|
||||
"InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.",
|
||||
"NextAiringDate": "Próxima Exibição: {date}"
|
||||
"NextAiringDate": "Próxima Exibição: {date}",
|
||||
"SeasonsMonitoredAll": "Todas",
|
||||
"SeasonsMonitoredPartial": "Parcial",
|
||||
"SeasonsMonitoredNone": "Nenhuma",
|
||||
"SeasonsMonitoredStatus": "Temporadas monitoradas",
|
||||
"NoBlocklistItems": "Sem itens na lista de bloqueio",
|
||||
"NotificationsTelegramSettingsMetadataLinks": "Links de Metadados",
|
||||
"NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações"
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
{
|
||||
default:
|
||||
case ImportMode.Auto:
|
||||
copyOnly = downloadClientItem != null && !downloadClientItem.CanMoveFiles;
|
||||
copyOnly = downloadClientItem is { CanMoveFiles: false };
|
||||
break;
|
||||
case ImportMode.Move:
|
||||
copyOnly = false;
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
@@ -29,6 +30,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
private readonly IAggregationService _aggregationService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IDetectSample _detectSample;
|
||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -37,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
IAggregationService aggregationService,
|
||||
IDiskProvider diskProvider,
|
||||
IDetectSample detectSample,
|
||||
ITrackedDownloadService trackedDownloadService,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
Logger logger)
|
||||
{
|
||||
@@ -45,6 +48,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
_aggregationService = aggregationService;
|
||||
_diskProvider = diskProvider;
|
||||
_detectSample = detectSample;
|
||||
_trackedDownloadService = trackedDownloadService;
|
||||
_formatCalculator = formatCalculator;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -145,6 +149,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
}
|
||||
else
|
||||
{
|
||||
if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true)
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.Find(downloadClientItem.DownloadId);
|
||||
|
||||
if (trackedDownload?.RemoteEpisode?.Release?.IndexerFlags != null)
|
||||
{
|
||||
localEpisode.IndexerFlags = trackedDownload.RemoteEpisode.Release.IndexerFlags;
|
||||
}
|
||||
}
|
||||
|
||||
localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode);
|
||||
localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
@@ -24,6 +25,7 @@ namespace NzbDrone.Core.Notifications.Emby
|
||||
var path = "/Notifications/Admin";
|
||||
var request = BuildRequest(path, settings);
|
||||
request.Headers.ContentType = "application/json";
|
||||
request.LogHttpError = false;
|
||||
|
||||
request.SetContent(new
|
||||
{
|
||||
@@ -32,7 +34,21 @@ namespace NzbDrone.Core.Notifications.Emby
|
||||
ImageUrl = "https://raw.github.com/Sonarr/Sonarr/develop/Logo/64.png"
|
||||
}.ToJson());
|
||||
|
||||
ProcessRequest(request, settings);
|
||||
try
|
||||
{
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
catch (HttpException e)
|
||||
{
|
||||
if (e.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.Warn("Unable to send notification to Emby. If you're using Jellyfin disable 'Send Notifications'");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HashSet<string> GetPaths(MediaBrowserSettings settings, Series series)
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Notifications.Emby
|
||||
public MediaBrowserSettings()
|
||||
{
|
||||
Port = 8096;
|
||||
UpdateLibrary = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host")]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
@@ -19,71 +20,77 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnGrab(GrabMessage grabMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE;
|
||||
var links = GetLinks(grabMessage.Series);
|
||||
|
||||
_proxy.SendNotification(title, grabMessage.Message, Settings);
|
||||
_proxy.SendNotification(title, grabMessage.Message, links, Settings);
|
||||
}
|
||||
|
||||
public override void OnDownload(DownloadMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE;
|
||||
var links = GetLinks(message.Series);
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
_proxy.SendNotification(title, message.Message, links, Settings);
|
||||
}
|
||||
|
||||
public override void OnImportComplete(ImportCompleteMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE;
|
||||
var links = GetLinks(message.Series);
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
_proxy.SendNotification(title, message.Message, links, Settings);
|
||||
}
|
||||
|
||||
public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE;
|
||||
var links = GetLinks(deleteMessage.Series);
|
||||
|
||||
_proxy.SendNotification(title, deleteMessage.Message, Settings);
|
||||
_proxy.SendNotification(title, deleteMessage.Message, links, Settings);
|
||||
}
|
||||
|
||||
public override void OnSeriesAdd(SeriesAddMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE;
|
||||
var links = GetLinks(message.Series);
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
_proxy.SendNotification(title, message.Message, links, Settings);
|
||||
}
|
||||
|
||||
public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE;
|
||||
var links = GetLinks(deleteMessage.Series);
|
||||
|
||||
_proxy.SendNotification(title, deleteMessage.Message, Settings);
|
||||
_proxy.SendNotification(title, deleteMessage.Message, links, Settings);
|
||||
}
|
||||
|
||||
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, healthCheck.Message, Settings);
|
||||
_proxy.SendNotification(title, healthCheck.Message, null, Settings);
|
||||
}
|
||||
|
||||
public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings);
|
||||
_proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", null, Settings);
|
||||
}
|
||||
|
||||
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, updateMessage.Message, Settings);
|
||||
_proxy.SendNotification(title, updateMessage.Message, null, Settings);
|
||||
}
|
||||
|
||||
public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
_proxy.SendNotification(title, message.Message, null, Settings);
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -94,5 +101,37 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
private List<TelegramLink> GetLinks(Series series)
|
||||
{
|
||||
var links = new List<TelegramLink>();
|
||||
|
||||
foreach (var link in Settings.MetadataLinks)
|
||||
{
|
||||
var linkType = (MetadataLinkType)link;
|
||||
|
||||
if (linkType == MetadataLinkType.Imdb && series.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
links.Add(new TelegramLink("IMDb", $"https://www.imdb.com/title/{series.ImdbId}"));
|
||||
}
|
||||
|
||||
if (linkType == MetadataLinkType.Tvdb && series.TvdbId > 0)
|
||||
{
|
||||
links.Add(new TelegramLink("TVDb", $"http://www.thetvdb.com/?tab=series&id={series.TvdbId}"));
|
||||
}
|
||||
|
||||
if (linkType == MetadataLinkType.Trakt && series.TvdbId > 0)
|
||||
{
|
||||
links.Add(new TelegramLink("TVMaze", $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show"));
|
||||
}
|
||||
|
||||
if (linkType == MetadataLinkType.Tvmaze && series.TvMazeId > 0)
|
||||
{
|
||||
links.Add(new TelegramLink("Trakt", $"http://www.tvmaze.com/shows/{series.TvMazeId}/_"));
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs
Normal file
14
src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public class TelegramLink
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public string Link { get; set; }
|
||||
|
||||
public TelegramLink(string label, string link)
|
||||
{
|
||||
Label = label;
|
||||
Link = link;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
@@ -13,7 +14,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public interface ITelegramProxy
|
||||
{
|
||||
void SendNotification(string title, string message, TelegramSettings settings);
|
||||
void SendNotification(string title, string message, List<TelegramLink> links, TelegramSettings settings);
|
||||
ValidationFailure Test(TelegramSettings settings);
|
||||
}
|
||||
|
||||
@@ -32,10 +33,16 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void SendNotification(string title, string message, TelegramSettings settings)
|
||||
public void SendNotification(string title, string message, List<TelegramLink> links, TelegramSettings settings)
|
||||
{
|
||||
// Format text to add the title before and bold using markdown
|
||||
var text = $"<b>{HttpUtility.HtmlEncode(title)}</b>\n{HttpUtility.HtmlEncode(message)}";
|
||||
var text = new StringBuilder($"<b>{HttpUtility.HtmlEncode(title)}</b>\n");
|
||||
|
||||
text.AppendLine(HttpUtility.HtmlEncode(message));
|
||||
|
||||
foreach (var link in links)
|
||||
{
|
||||
text.AppendLine($"<a href=\"{link.Link}\">{HttpUtility.HtmlEncode(link.Label)}</a>");
|
||||
}
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(URL).Resource("bot{token}/sendmessage").Post();
|
||||
|
||||
@@ -58,7 +65,12 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
const string title = "Test Notification";
|
||||
const string body = "This is a test message from Sonarr";
|
||||
|
||||
SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings);
|
||||
var links = new List<TelegramLink>
|
||||
{
|
||||
new TelegramLink("Sonarr.tv", "https://sonarr.tv")
|
||||
};
|
||||
|
||||
SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, links, settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
public class TelegramSettingsValidator : AbstractValidator<TelegramSettings>
|
||||
@@ -12,6 +14,16 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
RuleFor(c => c.ChatId).NotEmpty();
|
||||
RuleFor(c => c.TopicId).Must(topicId => !topicId.HasValue || topicId > 1)
|
||||
.WithMessage("Topic ID must be greater than 1 or empty");
|
||||
RuleFor(c => c.MetadataLinks).Custom((links, context) =>
|
||||
{
|
||||
foreach (var link in links)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(MetadataLinkType), link))
|
||||
{
|
||||
context.AddFailure("MetadataLinks", $"MetadataLink is not valid: {link}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +31,11 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
{
|
||||
private static readonly TelegramSettingsValidator Validator = new ();
|
||||
|
||||
public TelegramSettings()
|
||||
{
|
||||
MetadataLinks = Enumerable.Empty<int>();
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "NotificationsTelegramSettingsBotToken", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://core.telegram.org/bots")]
|
||||
public string BotToken { get; set; }
|
||||
|
||||
@@ -34,9 +51,27 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
[FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")]
|
||||
public bool IncludeAppNameInTitle { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")]
|
||||
public IEnumerable<int> MetadataLinks { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
|
||||
public enum MetadataLinkType
|
||||
{
|
||||
[FieldOption(Label = "IMDb")]
|
||||
Imdb,
|
||||
|
||||
[FieldOption(Label = "TVDb")]
|
||||
Tvdb,
|
||||
|
||||
[FieldOption(Label = "TVMaze")]
|
||||
Tvmaze,
|
||||
|
||||
[FieldOption(Label = "Trakt")]
|
||||
Trakt,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user