mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-16 21:15:28 -04:00
Compare commits
50 Commits
v4.0.8.200
...
v4.0.8.222
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14005d8d10 | ||
|
|
da7d17f5e8 | ||
|
|
ea331feb88 | ||
|
|
7dca9060ca | ||
|
|
8af12cc4e7 | ||
|
|
aa488019cf | ||
|
|
47a05ecb36 | ||
|
|
35baebaf72 | ||
|
|
aedcd046fc | ||
|
|
f45713bff8 | ||
|
|
911a3d4c1e | ||
|
|
e16ace54a8 | ||
|
|
84710a31bd | ||
|
|
093a239e77 | ||
|
|
ee69351733 | ||
|
|
e92a67ad78 | ||
|
|
3eca63a67c | ||
|
|
8484a8beba | ||
|
|
cd3a1c18ab | ||
|
|
dc7a16a03a | ||
|
|
84338f4c50 | ||
|
|
12ac123d5a | ||
|
|
ef829c6ace | ||
|
|
592b6f7f7c | ||
|
|
be5b449de4 | ||
|
|
9b144e9ade | ||
|
|
9af2f137f4 | ||
|
|
d4bd7865f6 | ||
|
|
cf921480ec | ||
|
|
639b53887d | ||
|
|
3b29096e40 | ||
|
|
2d237ae6b7 | ||
|
|
d713b83a36 | ||
|
|
2f04b037a1 | ||
|
|
7b87de2e93 | ||
|
|
eb2fd13509 | ||
|
|
ffdb08cfe6 | ||
|
|
37c4647f24 | ||
|
|
f7a58aab33 | ||
|
|
4b186e894e | ||
|
|
35a2bc9403 | ||
|
|
cc03ce04f1 | ||
|
|
363f8fc347 | ||
|
|
0877a6718d | ||
|
|
8b253c36ea | ||
|
|
e6f82270a9 | ||
|
|
813965e6a2 | ||
|
|
0d914f4c53 | ||
|
|
ae7f73208a | ||
|
|
4c86d673ea |
@@ -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..."
|
||||
|
||||
@@ -134,6 +134,12 @@ module.exports = (env) => {
|
||||
{
|
||||
source: 'frontend/src/Content/robots.txt',
|
||||
destination: path.join(distFolder, 'Content/robots.txt')
|
||||
},
|
||||
|
||||
// manifest.json and browserconfig.xml
|
||||
{
|
||||
source: 'frontend/src/Content/*.(json|xml)',
|
||||
destination: path.join(distFolder, 'Content')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const mixinsFiles = [
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'autoprefixer',
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
|
||||
@@ -51,7 +51,7 @@ function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||
sourceTitle,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed,
|
||||
isMarkingAsFailed = false,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
onMarkAsFailedPress,
|
||||
@@ -93,8 +93,4 @@ function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||
);
|
||||
}
|
||||
|
||||
HistoryDetailsModal.defaultProps = {
|
||||
isMarkingAsFailed: false,
|
||||
};
|
||||
|
||||
export default HistoryDetailsModal;
|
||||
|
||||
@@ -15,6 +15,7 @@ import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||
import useEpisode from 'Episode/useEpisode';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||
import useSeries from 'Series/useSeries';
|
||||
@@ -31,7 +32,7 @@ interface HistoryRowProps {
|
||||
id: number;
|
||||
episodeId: number;
|
||||
seriesId: number;
|
||||
languages: object[];
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
@@ -61,7 +62,7 @@ function HistoryRow(props: HistoryRowProps) {
|
||||
date,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed,
|
||||
isMarkingAsFailed = false,
|
||||
markAsFailedError,
|
||||
columns,
|
||||
} = props;
|
||||
@@ -268,8 +269,4 @@ function HistoryRow(props: HistoryRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
HistoryRow.defaultProps = {
|
||||
customFormats: [],
|
||||
};
|
||||
|
||||
export default HistoryRow;
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -155,10 +155,4 @@ function QueueStatus(props: QueueStatusProps) {
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatus.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading',
|
||||
canFlip: false,
|
||||
};
|
||||
|
||||
export default QueueStatus;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import DocumentTitle from 'react-document-title';
|
||||
import { Provider } from 'react-redux';
|
||||
@@ -20,7 +19,7 @@ function App({ store, history }: AppProps) {
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
<AppRoutes />
|
||||
</PageConnector>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
@@ -28,9 +27,4 @@ function App({ store, history }: AppProps) {
|
||||
);
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||
@@ -165,8 +164,4 @@ function AppRoutes() {
|
||||
);
|
||||
}
|
||||
|
||||
AppRoutes.propTypes = {
|
||||
app: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
|
||||
@@ -24,7 +24,9 @@ export interface DownloadClientAppState
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
export interface GeneralAppState
|
||||
extends AppSectionItemState<General>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
|
||||
@@ -8,15 +8,15 @@ import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||
export type HealthAppState = AppSectionState<Health>;
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
export type TaskAppState = AppSectionState<Task>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
diskSpace: DiskSpaceAppState;
|
||||
health: HealthAppState;
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
tasks: TaskAppState;
|
||||
updates: UpdateAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface CommandBody {
|
||||
seriesId?: number;
|
||||
seriesIds?: number[];
|
||||
seasonNumber?: number;
|
||||
[key: string]: string | number | boolean | undefined | number[] | undefined;
|
||||
[key: string]: string | number | boolean | number[] | undefined;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
||||
@@ -12,7 +12,7 @@ import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValu
|
||||
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue';
|
||||
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
|
||||
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
||||
@@ -78,7 +78,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
return QualityFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValueConnector;
|
||||
return QualityProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.SEASONS_MONITORED_STATUS:
|
||||
return SeasonsMonitoredStatusFilterBuilderRowValue;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createQualityProfilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function QualityProfileFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
const qualityProfiles = useSelector(createQualityProfilesSelector());
|
||||
|
||||
const tagList = qualityProfiles
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
.sort(sortByProp('name'));
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default QualityProfileFilterBuilderRowValue;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = qualityProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
||||
@@ -46,9 +46,9 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
||||
const values = [...seriesTypeOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeNoChange = false,
|
||||
includeNoChangeDisabled = true,
|
||||
includeMixed,
|
||||
includeMixed = false,
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
@@ -77,9 +77,4 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
||||
);
|
||||
}
|
||||
|
||||
SeriesTypeSelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false,
|
||||
};
|
||||
|
||||
export default SeriesTypeSelectInput;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './Label.css';
|
||||
|
||||
function Label(props) {
|
||||
const {
|
||||
className,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
children,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
Label.defaultProps = {
|
||||
className: styles.label,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.SMALL,
|
||||
outline: false
|
||||
};
|
||||
|
||||
export default Label;
|
||||
31
frontend/src/Components/Label.tsx
Normal file
31
frontend/src/Components/Label.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './Label.css';
|
||||
|
||||
export interface LabelProps extends ComponentProps<'span'> {
|
||||
kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>;
|
||||
size?: Extract<(typeof sizes.all)[number], keyof typeof styles>;
|
||||
outline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Label({
|
||||
className = styles.label,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.SMALL,
|
||||
outline = false,
|
||||
...otherProps
|
||||
}: LabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -212,6 +212,8 @@ class SignalRConnector extends Component {
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||
|
||||
repopulatePage('seriesUpdated');
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||
}
|
||||
|
||||
@@ -66,7 +66,9 @@ function Table(props) {
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
isVisible,
|
||||
isSortable,
|
||||
...otherColumnProps
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
@@ -84,6 +86,7 @@ function Table(props) {
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherProps}
|
||||
{...otherColumnProps}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
|
||||
<TileColor>#00ccff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "Sonarr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "standalone"
|
||||
}
|
||||
11
frontend/src/Content/browserconfig.xml
Normal file
11
frontend/src/Content/browserconfig.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="__URL_BASE__/Content/Images/Icons/mstile-150x150.png" />
|
||||
<TileColor>
|
||||
#00ccff
|
||||
</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
19
frontend/src/Content/manifest.json
Normal file
19
frontend/src/Content/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Sonarr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "__URL_BASE__/",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function EpisodeLanguages(props) {
|
||||
const {
|
||||
className,
|
||||
languages,
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
interface EpisodeLanguagesProps {
|
||||
className?: string;
|
||||
languages: Language[];
|
||||
isCutoffNotMet?: boolean;
|
||||
}
|
||||
|
||||
if (!languages) {
|
||||
function EpisodeLanguages(props: EpisodeLanguagesProps) {
|
||||
const { className, languages, isCutoffNotMet = true } = props;
|
||||
|
||||
// TODO: Typescript - Remove once everything is converted
|
||||
if (!languages || languages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -41,15 +44,9 @@ function EpisodeLanguages(props) {
|
||||
title={translate('Languages')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
languages.map((language) => {
|
||||
return (
|
||||
<li key={language.id}>
|
||||
{language.name}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
{languages.map((language) => (
|
||||
<li key={language.id}>{language.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
@@ -57,14 +54,4 @@ function EpisodeLanguages(props) {
|
||||
);
|
||||
}
|
||||
|
||||
EpisodeLanguages.propTypes = {
|
||||
className: PropTypes.string,
|
||||
languages: PropTypes.arrayOf(PropTypes.object),
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
};
|
||||
|
||||
EpisodeLanguages.defaultProps = {
|
||||
isCutoffNotMet: true
|
||||
};
|
||||
|
||||
export default EpisodeLanguages;
|
||||
@@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
||||
27
frontend/src/FirstRun/AuthenticationRequiredModal.tsx
Normal file
27
frontend/src/FirstRun/AuthenticationRequiredModal.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
interface AuthenticationRequiredModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function AuthenticationRequiredModal({
|
||||
isOpen,
|
||||
}: AuthenticationRequiredModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
dispatchFetchStatus
|
||||
} = props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && didMount.current) {
|
||||
dispatchFetchStatus();
|
||||
}
|
||||
|
||||
didMount.current = true;
|
||||
}, [isSaving, dispatchFetchStatus]);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('AuthenticationRequired')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||
helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModalContent;
|
||||
194
frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx
Normal file
194
frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
authenticationMethodOptions,
|
||||
authenticationRequiredOptions,
|
||||
} from 'Settings/General/SecuritySettings';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchGeneralSettings,
|
||||
saveGeneralSettings,
|
||||
setGeneralSettingsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
const selector = createSettingsSectionSelector(SECTION);
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
export default function AuthenticationRequiredModalContent() {
|
||||
const { isPopulated, error, isSaving, settings } = useSelector(selector);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
} = settings;
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchGeneralSettings());
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(args: InputChanged) => {
|
||||
// @ts-expect-error Actions aren't typed
|
||||
dispatch(setGeneralSettingsValue(args));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const authenticationEnabled =
|
||||
authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
useEffect(() => {
|
||||
if (isSaving || !wasSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchStatus());
|
||||
}, [isSaving, wasSaving, dispatch]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
dispatch(saveGeneralSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent showCloseButton={false} onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AuthenticationRequired')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert className={styles.authRequiredAlert} kind={kinds.WARNING}>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{isPopulated && !error ? (
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={
|
||||
authenticationMethod.value === 'none'
|
||||
? translate('AuthenticationMethodHelpTextWarning')
|
||||
: undefined
|
||||
}
|
||||
helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
helpTextWarning={
|
||||
username?.value
|
||||
? undefined
|
||||
: translate('AuthenticationRequiredUsernameHelpTextWarning')
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
helpTextWarning={
|
||||
password?.value
|
||||
? undefined
|
||||
: translate('AuthenticationRequiredPasswordHelpTextWarning')
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
helpTextWarning={
|
||||
passwordConfirmation?.value
|
||||
? undefined
|
||||
: translate(
|
||||
'AuthenticationRequiredPasswordConfirmationHelpTextWarning'
|
||||
)
|
||||
}
|
||||
onChange={onInputChange}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onPress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(sectionSettings) => {
|
||||
return {
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchClearPendingChanges: clearPendingChanges,
|
||||
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||
dispatchFetchStatus: fetchStatus
|
||||
};
|
||||
|
||||
class AuthenticationRequiredModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveGeneralSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchFetchGeneralSettings,
|
||||
dispatchSetGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AuthenticationRequiredModalContent
|
||||
{...otherProps}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||
@@ -19,5 +19,5 @@ export const all = [
|
||||
PRIMARY,
|
||||
PURPLE,
|
||||
SUCCESS,
|
||||
WARNING
|
||||
];
|
||||
WARNING,
|
||||
] as const;
|
||||
@@ -4,4 +4,12 @@ export const MEDIUM = 'medium';
|
||||
export const LARGE = 'large';
|
||||
export const EXTRA_LARGE = 'extraLarge';
|
||||
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];
|
||||
|
||||
export const all = [
|
||||
EXTRA_SMALL,
|
||||
SMALL,
|
||||
MEDIUM,
|
||||
LARGE,
|
||||
EXTRA_LARGE,
|
||||
EXTRA_EXTRA_LARGE,
|
||||
] as const;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -130,6 +130,7 @@
|
||||
.sizeOnDisk,
|
||||
.qualityProfileName,
|
||||
.originalLanguageName,
|
||||
.statusName,
|
||||
.network,
|
||||
.links,
|
||||
.tags {
|
||||
|
||||
@@ -24,6 +24,7 @@ interface CssExports {
|
||||
'seriesNavigationButton': string;
|
||||
'seriesNavigationButtons': string;
|
||||
'sizeOnDisk': string;
|
||||
'statusName': string;
|
||||
'tags': string;
|
||||
'title': string;
|
||||
'titleContainer': string;
|
||||
|
||||
@@ -212,8 +212,8 @@ class SeriesDetails extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
episodeFileCount,
|
||||
sizeOnDisk
|
||||
episodeFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -230,7 +230,7 @@ class SeriesDetails extends Component {
|
||||
} = this.state;
|
||||
|
||||
const statusDetails = getSeriesStatusDetails(status);
|
||||
const runningYears = statusDetails.title === translate('Ended') ? `${year}-${getDateYear(lastAired)}` : `${year}-`;
|
||||
const runningYears = status === 'ended' ? `${year}-${getDateYear(lastAired)}` : `${year}-`;
|
||||
|
||||
let episodeFilesCountMessage = translate('SeriesDetailsNoEpisodeFiles');
|
||||
|
||||
@@ -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>
|
||||
@@ -510,13 +509,14 @@ class SeriesDetails extends Component {
|
||||
className={styles.detailsLabel}
|
||||
title={statusDetails.message}
|
||||
size={sizes.LARGE}
|
||||
kind={status === 'deleted' ? kinds.INVERSE : undefined}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
name={statusDetails.icon}
|
||||
size={17}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
<span className={styles.statusName}>
|
||||
{statusDetails.title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +152,7 @@ class SeriesDetailsConnector extends Component {
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.populate);
|
||||
registerPagePopulator(this.populate, ['seriesUpdated']);
|
||||
this.populate();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ $hoverScale: 1.05;
|
||||
}
|
||||
}
|
||||
|
||||
.ended {
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@@ -34,8 +34,15 @@ $hoverScale: 1.05;
|
||||
height: 0;
|
||||
border-width: 0 25px 25px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent var(--dangerColor) transparent transparent;
|
||||
color: var(--white);
|
||||
|
||||
&.ended {
|
||||
border-color: transparent var(--dangerColor) transparent transparent;
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
border-color: transparent var(--gray) transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'content': string;
|
||||
'deleted': string;
|
||||
'details': string;
|
||||
'ended': string;
|
||||
'info': string;
|
||||
@@ -11,6 +12,7 @@ interface CssExports {
|
||||
'overviewContainer': string;
|
||||
'poster': string;
|
||||
'posterContainer': string;
|
||||
'status': string;
|
||||
'tags': string;
|
||||
'title': string;
|
||||
'titleRow': string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
@@ -146,9 +147,19 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
|
||||
<SeriesIndexPosterSelect seriesId={seriesId} />
|
||||
) : null}
|
||||
|
||||
{status === 'ended' && (
|
||||
<div className={styles.ended} title={translate('Ended')} />
|
||||
)}
|
||||
{status === 'ended' ? (
|
||||
<div
|
||||
className={classNames(styles.status, styles.ended)}
|
||||
title={translate('Ended')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{status === 'deleted' ? (
|
||||
<div
|
||||
className={classNames(styles.status, styles.deleted)}
|
||||
title={translate('Deleted')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Link className={styles.link} style={elementStyle} to={link}>
|
||||
<SeriesPoster
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ $hoverScale: 1.05;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ended {
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@@ -80,8 +80,15 @@ $hoverScale: 1.05;
|
||||
height: 0;
|
||||
border-width: 0 25px 25px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent var(--dangerColor) transparent transparent;
|
||||
color: var(--white);
|
||||
|
||||
&.ended {
|
||||
border-color: transparent var(--dangerColor) transparent transparent;
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
border-color: transparent var(--gray) transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
|
||||
@@ -5,11 +5,13 @@ interface CssExports {
|
||||
'container': string;
|
||||
'content': string;
|
||||
'controls': string;
|
||||
'deleted': string;
|
||||
'ended': string;
|
||||
'link': string;
|
||||
'nextAiring': string;
|
||||
'overlayTitle': string;
|
||||
'posterContainer': string;
|
||||
'status': string;
|
||||
'tags': string;
|
||||
'tagsList': string;
|
||||
'title': string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
|
||||
@@ -161,7 +162,17 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
||||
</Label>
|
||||
|
||||
{status === 'ended' ? (
|
||||
<div className={styles.ended} title={translate('Ended')} />
|
||||
<div
|
||||
className={classNames(styles.status, styles.ended)}
|
||||
title={translate('Ended')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{status === 'deleted' ? (
|
||||
<div
|
||||
className={classNames(styles.status, styles.deleted)}
|
||||
title={translate('Deleted')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Link className={styles.link} style={elementStyle} to={link}>
|
||||
|
||||
@@ -37,7 +37,7 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
|
||||
added,
|
||||
seasonCount,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
sizeOnDisk = 0,
|
||||
tags,
|
||||
sortKey,
|
||||
showRelativeDates,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sizes } from 'Helpers/Props';
|
||||
import createSeriesQueueItemsDetailsSelector, {
|
||||
SeriesQueueDetails,
|
||||
} from 'Series/Index/createSeriesQueueDetailsSelector';
|
||||
import { SeriesStatus } from 'Series/Series';
|
||||
import getProgressBarKind from 'Utilities/Series/getProgressBarKind';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SeriesIndexProgressBar.css';
|
||||
@@ -13,7 +14,7 @@ interface SeriesIndexProgressBarProps {
|
||||
seriesId: number;
|
||||
seasonNumber?: number;
|
||||
monitored: boolean;
|
||||
status: string;
|
||||
status: SeriesStatus;
|
||||
episodeCount: number;
|
||||
episodeFileCount: number;
|
||||
totalEpisodeCount: number;
|
||||
|
||||
@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { SeriesStatus } from 'Series/Series';
|
||||
import { getSeriesStatusDetails } from 'Series/SeriesStatus';
|
||||
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -13,7 +14,7 @@ interface SeriesStatusCellProps {
|
||||
className: string;
|
||||
seriesId: number;
|
||||
monitored: boolean;
|
||||
status: string;
|
||||
status: SeriesStatus;
|
||||
isSelectMode: boolean;
|
||||
isSaving: boolean;
|
||||
component?: React.ElementType;
|
||||
|
||||
@@ -15,6 +15,8 @@ export type SeriesMonitor =
|
||||
| 'unmonitorSpecials'
|
||||
| 'none';
|
||||
|
||||
export type SeriesStatus = 'continuing' | 'ended' | 'upcoming' | 'deleted';
|
||||
|
||||
export type MonitorNewItems = 'all' | 'none';
|
||||
|
||||
export interface Image {
|
||||
@@ -86,7 +88,7 @@ interface Series extends ModelBase {
|
||||
seriesType: SeriesType;
|
||||
sortTitle: string;
|
||||
statistics: Statistics;
|
||||
status: string;
|
||||
status: SeriesStatus;
|
||||
tags: number[];
|
||||
title: string;
|
||||
titleSlug: string;
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { SeriesStatus } from 'Series/Series';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export function getSeriesStatusDetails(status) {
|
||||
|
||||
export function getSeriesStatusDetails(status: SeriesStatus) {
|
||||
let statusDetails = {
|
||||
icon: icons.SERIES_CONTINUING,
|
||||
title: translate('Continuing'),
|
||||
message: translate('ContinuingSeriesDescription')
|
||||
message: translate('ContinuingSeriesDescription'),
|
||||
};
|
||||
|
||||
if (status === 'deleted') {
|
||||
statusDetails = {
|
||||
icon: icons.SERIES_DELETED,
|
||||
title: translate('Deleted'),
|
||||
message: translate('DeletedSeriesDescription')
|
||||
message: translate('DeletedSeriesDescription'),
|
||||
};
|
||||
} else if (status === 'ended') {
|
||||
statusDetails = {
|
||||
icon: icons.SERIES_ENDED,
|
||||
title: translate('Ended'),
|
||||
message: translate('EndedSeriesDescription')
|
||||
message: translate('EndedSeriesDescription'),
|
||||
};
|
||||
} else if (status === 'upcoming') {
|
||||
statusDetails = {
|
||||
icon: icons.SERIES_CONTINUING,
|
||||
title: translate('Upcoming'),
|
||||
message: translate('UpcomingSeriesDescription')
|
||||
message: translate('UpcomingSeriesDescription'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
||||
function SeriesTitleLink({ titleSlug, title }) {
|
||||
const link = `/series/${titleSlug}`;
|
||||
|
||||
return (
|
||||
<Link to={link}>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
SeriesTitleLink.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default SeriesTitleLink;
|
||||
21
frontend/src/Series/SeriesTitleLink.tsx
Normal file
21
frontend/src/Series/SeriesTitleLink.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import Link, { LinkProps } from 'Components/Link/Link';
|
||||
|
||||
export interface SeriesTitleLinkProps extends LinkProps {
|
||||
titleSlug: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function SeriesTitleLink({
|
||||
titleSlug,
|
||||
title,
|
||||
...linkProps
|
||||
}: SeriesTitleLinkProps) {
|
||||
const link = `/series/${titleSlug}`;
|
||||
|
||||
return (
|
||||
<Link to={link} {...linkProps}>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -157,6 +157,7 @@ class GeneralSettings extends Component {
|
||||
/>
|
||||
|
||||
<LoggingSettings
|
||||
advancedSettings={advancedSettings}
|
||||
settings={settings}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -30,12 +30,14 @@ const logLevelOptions = [
|
||||
|
||||
function LoggingSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
const {
|
||||
logLevel
|
||||
logLevel,
|
||||
logSizeLimit
|
||||
} = settings;
|
||||
|
||||
return (
|
||||
@@ -52,11 +54,30 @@ function LoggingSettings(props) {
|
||||
{...logLevel}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('LogSizeLimit')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="logSizeLimit"
|
||||
min={1}
|
||||
max={10}
|
||||
unit="MB"
|
||||
helpText={translate('LogSizeLimitHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...logSizeLimit}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
LoggingSettings.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ function EditImportListExclusionModal(
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
|
||||
<EditImportListExclusionModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
onModalClose={onModalClosePress}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,21 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { deleteImportListExclusion } from 'Store/Actions/Settings/importListExclusions';
|
||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditImportListExclusionModal from './EditImportListExclusionModal';
|
||||
import styles from './ImportListExclusionRow.css';
|
||||
|
||||
interface ImportListExclusionRowProps extends ImportListExclusion {
|
||||
onConfirmDeleteImportListExclusion: (id: number) => void;
|
||||
isSelected: boolean;
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function ImportListExclusionRow(props: ImportListExclusionRowProps) {
|
||||
const { id, title, tvdbId, onConfirmDeleteImportListExclusion } = props;
|
||||
const { id, tvdbId, title, isSelected, onSelectedChange } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [
|
||||
isEditImportListExclusionModalOpen,
|
||||
@@ -29,12 +36,18 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) {
|
||||
setDeleteImportListExclusionModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const onConfirmDeleteImportListExclusionPress = useCallback(() => {
|
||||
onConfirmDeleteImportListExclusion(id);
|
||||
}, [id, onConfirmDeleteImportListExclusion]);
|
||||
const handleDeletePress = useCallback(() => {
|
||||
dispatch(deleteImportListExclusion({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<TableRowCell>{title}</TableRowCell>
|
||||
<TableRowCell>{tvdbId}</TableRowCell>
|
||||
|
||||
@@ -58,7 +71,7 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) {
|
||||
title={translate('DeleteImportListExclusion')}
|
||||
message={translate('DeleteImportListExclusionMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={onConfirmDeleteImportListExclusionPress}
|
||||
onConfirm={handleDeletePress}
|
||||
onCancel={setDeleteImportListExclusionModalClosed}
|
||||
/>
|
||||
</TableRow>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.actions {
|
||||
composes: headerCell from '~Components/Table/TableHeaderCell.css';
|
||||
|
||||
width: 35px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'addButton': string;
|
||||
'addImportListExclusion': string;
|
||||
'actions': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import usePaging from 'Components/Table/usePaging';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import * as importListExclusionActions from 'Store/Actions/Settings/importListExclusions';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
bulkDeleteImportListExclusions,
|
||||
clearImportListExclusions,
|
||||
fetchImportListExclusions,
|
||||
gotoImportListExclusionPage,
|
||||
setImportListExclusionSort,
|
||||
setImportListExclusionTableOption,
|
||||
} from 'Store/Actions/Settings/importListExclusions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import EditImportListExclusionModal from './EditImportListExclusionModal';
|
||||
import ImportListExclusionRow from './ImportListExclusionRow';
|
||||
import styles from './ImportListExclusions.css';
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
@@ -36,13 +54,15 @@ const COLUMNS = [
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
className: styles.actions,
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
isSortable: false,
|
||||
},
|
||||
];
|
||||
|
||||
function createImportListExlucionsSelector() {
|
||||
function createImportListExclusionsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.importListExclusions,
|
||||
(importListExclusions) => {
|
||||
@@ -54,95 +74,7 @@ function createImportListExlucionsSelector() {
|
||||
}
|
||||
|
||||
function ImportListExclusions() {
|
||||
const history = useHistory();
|
||||
const useCurrentPage = history.action === 'POP';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const fetchImportListExclusions = useCallback(() => {
|
||||
dispatch(importListExclusionActions.fetchImportListExclusions());
|
||||
}, [dispatch]);
|
||||
|
||||
const deleteImportListExclusion = useCallback(
|
||||
(payload: { id: number }) => {
|
||||
dispatch(importListExclusionActions.deleteImportListExclusion(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const gotoImportListExclusionFirstPage = useCallback(() => {
|
||||
dispatch(importListExclusionActions.gotoImportListExclusionFirstPage());
|
||||
}, [dispatch]);
|
||||
|
||||
const gotoImportListExclusionPreviousPage = useCallback(() => {
|
||||
dispatch(importListExclusionActions.gotoImportListExclusionPreviousPage());
|
||||
}, [dispatch]);
|
||||
|
||||
const gotoImportListExclusionNextPage = useCallback(() => {
|
||||
dispatch(importListExclusionActions.gotoImportListExclusionNextPage());
|
||||
}, [dispatch]);
|
||||
|
||||
const gotoImportListExclusionLastPage = useCallback(() => {
|
||||
dispatch(importListExclusionActions.gotoImportListExclusionLastPage());
|
||||
}, [dispatch]);
|
||||
|
||||
const gotoImportListExclusionPage = useCallback(
|
||||
(page: number) => {
|
||||
dispatch(
|
||||
importListExclusionActions.gotoImportListExclusionPage({ page })
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setImportListExclusionSort = useCallback(
|
||||
(sortKey: { sortKey: string }) => {
|
||||
dispatch(
|
||||
importListExclusionActions.setImportListExclusionSort({ sortKey })
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setImportListTableOption = useCallback(
|
||||
(payload: { pageSize: number }) => {
|
||||
dispatch(
|
||||
importListExclusionActions.setImportListExclusionTableOption(payload)
|
||||
);
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(importListExclusionActions.gotoImportListExclusionFirstPage());
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const repopulate = useCallback(() => {
|
||||
gotoImportListExclusionFirstPage();
|
||||
}, [gotoImportListExclusionFirstPage]);
|
||||
|
||||
useEffect(() => {
|
||||
registerPagePopulator(repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchImportListExclusions();
|
||||
} else {
|
||||
gotoImportListExclusionFirstPage();
|
||||
}
|
||||
|
||||
return () => unregisterPagePopulator(repopulate);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onConfirmDeleteImportListExclusion = useCallback(
|
||||
(id: number) => {
|
||||
deleteImportListExclusion({ id });
|
||||
repopulate();
|
||||
},
|
||||
[deleteImportListExclusion, repopulate]
|
||||
);
|
||||
|
||||
const selected = useSelector(createImportListExlucionsSelector());
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
@@ -152,9 +84,127 @@ function ImportListExclusions() {
|
||||
sortKey,
|
||||
error,
|
||||
sortDirection,
|
||||
page,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
...otherProps
|
||||
} = selected;
|
||||
isDeleting,
|
||||
deleteError,
|
||||
} = useSelector(createImportListExclusionsSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
|
||||
useState(false);
|
||||
const previousIsDeleting = usePrevious(isDeleting);
|
||||
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleDeleteSelectedPress = useCallback(() => {
|
||||
setIsConfirmDeleteModalOpen(true);
|
||||
}, [setIsConfirmDeleteModalOpen]);
|
||||
|
||||
const handleDeleteSelectedConfirmed = useCallback(() => {
|
||||
dispatch(bulkDeleteImportListExclusions({ ids: selectedIds }));
|
||||
setIsConfirmDeleteModalOpen(false);
|
||||
}, [selectedIds, setIsConfirmDeleteModalOpen, dispatch]);
|
||||
|
||||
const handleConfirmDeleteModalClose = useCallback(() => {
|
||||
setIsConfirmDeleteModalOpen(false);
|
||||
}, [setIsConfirmDeleteModalOpen]);
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoImportListExclusionPage,
|
||||
});
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: { sortKey: string }) => {
|
||||
dispatch(setImportListExclusionSort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setImportListExclusionTableOption(payload));
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoImportListExclusionPage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchImportListExclusions());
|
||||
} else {
|
||||
dispatch(gotoImportListExclusionPage({ page: 1 }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(clearImportListExclusions());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchImportListExclusions());
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousIsDeleting && !isDeleting && !deleteError) {
|
||||
setSelectState({ type: 'unselectAll', items });
|
||||
|
||||
dispatch(fetchImportListExclusions());
|
||||
}
|
||||
}, [
|
||||
previousIsDeleting,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
items,
|
||||
dispatch,
|
||||
setSelectState,
|
||||
]);
|
||||
|
||||
const [
|
||||
isAddImportListExclusionModalOpen,
|
||||
@@ -173,13 +223,17 @@ function ImportListExclusions() {
|
||||
error={error}
|
||||
>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={COLUMNS}
|
||||
canModifyColumns={false}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={setImportListExclusionSort}
|
||||
onTableOptionChange={setImportListTableOption}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
@@ -187,16 +241,23 @@ function ImportListExclusions() {
|
||||
<ImportListExclusionRow
|
||||
key={item.id}
|
||||
{...item}
|
||||
onConfirmDeleteImportListExclusion={
|
||||
onConfirmDeleteImportListExclusion
|
||||
}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<TableRow>
|
||||
<TableRowCell />
|
||||
<TableRowCell />
|
||||
<TableRowCell colSpan={3}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedIds.length}
|
||||
onPress={handleDeleteSelectedPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<IconButton
|
||||
@@ -209,21 +270,31 @@ function ImportListExclusions() {
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
pageSize={pageSize}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={gotoImportListExclusionFirstPage}
|
||||
onPreviousPagePress={gotoImportListExclusionPreviousPage}
|
||||
onNextPagePress={gotoImportListExclusionNextPage}
|
||||
onLastPagePress={gotoImportListExclusionLastPage}
|
||||
onPageSelect={gotoImportListExclusionPage}
|
||||
{...otherProps}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
/>
|
||||
|
||||
<EditImportListExclusionModal
|
||||
isOpen={isAddImportListExclusionModalOpen}
|
||||
onModalClose={setAddImportListExclusionModalClosed}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteSelected')}
|
||||
message={translate('DeleteSelectedImportListExclusionsMessageText')}
|
||||
confirmLabel={translate('DeleteSelected')}
|
||||
onConfirm={handleDeleteSelectedConfirmed}
|
||||
onCancel={handleConfirmDeleteModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportListsExclusions from './ImportListExclusions/ImportListExclusions';
|
||||
import ImportListExclusions from './ImportListExclusions/ImportListExclusions';
|
||||
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
||||
import ImportListOptions from './Options/ImportListOptions';
|
||||
@@ -113,7 +113,8 @@ class ImportListSettings extends Component {
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
|
||||
<ImportListsExclusions />
|
||||
<ImportListExclusions />
|
||||
|
||||
<ManageImportListsModal
|
||||
isOpen={isManageImportListsOpen}
|
||||
onModalClose={this.onManageImportListsModalClose}
|
||||
|
||||
@@ -21,7 +21,7 @@ const mapDispatchToProps = {
|
||||
fetchRootFolders
|
||||
};
|
||||
|
||||
class ListsConnector extends Component {
|
||||
class ImportListsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -51,10 +51,10 @@ class ListsConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
ListsConnector.propTypes = {
|
||||
ImportListsConnector.propTypes = {
|
||||
fetchImportLists: PropTypes.func.isRequired,
|
||||
deleteImportList: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ListsConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListsConnector);
|
||||
|
||||
@@ -48,7 +48,6 @@ interface ImportListOptionsPageProps {
|
||||
|
||||
function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
const { setChildSave, onChildStateChange } = props;
|
||||
const selected = useSelector(createImportListOptionsSelector());
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
@@ -58,7 +57,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
} = selected;
|
||||
} = useSelector(createImportListOptionsSelector());
|
||||
|
||||
const { listSyncLevel, listSyncTag } = settings;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import QualityDefinitionLimits from './QualityDefinitionLimits';
|
||||
import styles from './QualityDefinition.css';
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 400;
|
||||
const MAX = 1000;
|
||||
const MIN_DISTANCE = 1;
|
||||
|
||||
const slider = {
|
||||
|
||||
@@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
|
||||
return function(getState, payload, dispatch) {
|
||||
const {
|
||||
id,
|
||||
...queryParams
|
||||
queryParams
|
||||
} = payload;
|
||||
|
||||
dispatch(set({ section, isDeleting: true }));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers';
|
||||
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
@@ -16,29 +18,26 @@ const section = 'settings.importListExclusions';
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions';
|
||||
export const GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionFirstPage';
|
||||
export const GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPreviousPage';
|
||||
export const GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionNextPage';
|
||||
export const GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionLastPage';
|
||||
export const GOTO_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPage';
|
||||
export const SET_IMPORT_LIST_EXCLUSION_SORT = 'settings/importListExclusions/setImportListExclusionSort';
|
||||
export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption';
|
||||
export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion';
|
||||
export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion';
|
||||
export const BULK_DELETE_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/bulkDeleteImportListExclusions';
|
||||
export const CLEAR_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/clearImportListExclusions';
|
||||
|
||||
export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption';
|
||||
export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS);
|
||||
export const gotoImportListExclusionFirstPage = createThunk(GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE);
|
||||
export const gotoImportListExclusionPreviousPage = createThunk(GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE);
|
||||
export const gotoImportListExclusionNextPage = createThunk(GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE);
|
||||
export const gotoImportListExclusionLastPage = createThunk(GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE);
|
||||
export const gotoImportListExclusionPage = createThunk(GOTO_IMPORT_LIST_EXCLUSION_PAGE);
|
||||
export const setImportListExclusionSort = createThunk(SET_IMPORT_LIST_EXCLUSION_SORT);
|
||||
export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION);
|
||||
export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION);
|
||||
export const bulkDeleteImportListExclusions = createThunk(BULK_DELETE_IMPORT_LIST_EXCLUSIONS);
|
||||
export const clearImportListExclusions = createAction(CLEAR_IMPORT_LIST_EXCLUSIONS);
|
||||
|
||||
export const setImportListExclusionTableOption = createAction(SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION);
|
||||
export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => {
|
||||
@@ -64,6 +63,8 @@ export default {
|
||||
items: [],
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
@@ -77,16 +78,13 @@ export default {
|
||||
fetchImportListExclusions,
|
||||
{
|
||||
[serverSideCollectionHandlers.FETCH]: FETCH_IMPORT_LIST_EXCLUSIONS,
|
||||
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE,
|
||||
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE,
|
||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE,
|
||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE,
|
||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_IMPORT_LIST_EXCLUSION_PAGE,
|
||||
[serverSideCollectionHandlers.SORT]: SET_IMPORT_LIST_EXCLUSION_SORT
|
||||
}
|
||||
),
|
||||
[SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'),
|
||||
[DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion')
|
||||
[DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion'),
|
||||
[BULK_DELETE_IMPORT_LIST_EXCLUSIONS]: createBulkRemoveItemHandler(section, '/importlistexclusion/bulk')
|
||||
}),
|
||||
|
||||
//
|
||||
@@ -94,7 +92,19 @@ export default {
|
||||
|
||||
reducers: {
|
||||
[SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section),
|
||||
[SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section)
|
||||
[SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section),
|
||||
|
||||
[CLEAR_IMPORT_LIST_EXCLUSIONS]: createClearReducer(section, {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: [],
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
pendingChanges: {},
|
||||
totalPages: 0,
|
||||
totalRecords: 0
|
||||
})
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -20,19 +20,19 @@ const section = 'settings.importLists';
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_IMPORT_LISTS = 'settings/importlists/fetchImportLists';
|
||||
export const FETCH_IMPORT_LIST_SCHEMA = 'settings/importlists/fetchImportListSchema';
|
||||
export const SELECT_IMPORT_LIST_SCHEMA = 'settings/importlists/selectImportListSchema';
|
||||
export const SET_IMPORT_LIST_VALUE = 'settings/importlists/setImportListValue';
|
||||
export const SET_IMPORT_LIST_FIELD_VALUE = 'settings/importlists/setImportListFieldValue';
|
||||
export const SAVE_IMPORT_LIST = 'settings/importlists/saveImportList';
|
||||
export const CANCEL_SAVE_IMPORT_LIST = 'settings/importlists/cancelSaveImportList';
|
||||
export const DELETE_IMPORT_LIST = 'settings/importlists/deleteImportList';
|
||||
export const TEST_IMPORT_LIST = 'settings/importlists/testImportList';
|
||||
export const CANCEL_TEST_IMPORT_LIST = 'settings/importlists/cancelTestImportList';
|
||||
export const TEST_ALL_IMPORT_LISTS = 'settings/importlists/testAllImportLists';
|
||||
export const BULK_EDIT_IMPORT_LISTS = 'settings/importlists/bulkEditImportLists';
|
||||
export const BULK_DELETE_IMPORT_LISTS = 'settings/importlists/bulkDeleteImportLists';
|
||||
export const FETCH_IMPORT_LISTS = 'settings/importLists/fetchImportLists';
|
||||
export const FETCH_IMPORT_LIST_SCHEMA = 'settings/importLists/fetchImportListSchema';
|
||||
export const SELECT_IMPORT_LIST_SCHEMA = 'settings/importLists/selectImportListSchema';
|
||||
export const SET_IMPORT_LIST_VALUE = 'settings/importLists/setImportListValue';
|
||||
export const SET_IMPORT_LIST_FIELD_VALUE = 'settings/importLists/setImportListFieldValue';
|
||||
export const SAVE_IMPORT_LIST = 'settings/importLists/saveImportList';
|
||||
export const CANCEL_SAVE_IMPORT_LIST = 'settings/importLists/cancelSaveImportList';
|
||||
export const DELETE_IMPORT_LIST = 'settings/importLists/deleteImportList';
|
||||
export const TEST_IMPORT_LIST = 'settings/importLists/testImportList';
|
||||
export const CANCEL_TEST_IMPORT_LIST = 'settings/importLists/cancelTestImportList';
|
||||
export const TEST_ALL_IMPORT_LISTS = 'settings/importLists/testAllImportLists';
|
||||
export const BULK_EDIT_IMPORT_LISTS = 'settings/importLists/bulkEditImportLists';
|
||||
export const BULK_DELETE_IMPORT_LISTS = 'settings/importLists/bulkDeleteImportLists';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
@@ -77,7 +77,7 @@ export default {
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
method: 'PUT',
|
||||
url: '/qualityDefinition/update',
|
||||
url: '/qualitydefinition/update',
|
||||
data: JSON.stringify(upatedDefinitions),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json'
|
||||
|
||||
@@ -269,8 +269,9 @@ export const defaultState = {
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'releases.selectedFilterKey',
|
||||
'releases.episode.selectedFilterKey',
|
||||
'releases.episode.customFilters',
|
||||
'releases.season.selectedFilterKey',
|
||||
'releases.season.customFilters'
|
||||
];
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ function DiskSpace() {
|
||||
const { freeSpace, totalSpace } = item;
|
||||
|
||||
const diskUsage = 100 - (freeSpace / totalSpace) * 100;
|
||||
let diskUsageKind = kinds.PRIMARY;
|
||||
let diskUsageKind: (typeof kinds.all)[number] = kinds.PRIMARY;
|
||||
|
||||
if (diskUsage > 90) {
|
||||
diskUsageKind = kinds.DANGER;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -7,22 +7,17 @@ import DiskSpace from './DiskSpace/DiskSpace';
|
||||
import Health from './Health/Health';
|
||||
import MoreInfo from './MoreInfo/MoreInfo';
|
||||
|
||||
class Status extends Component {
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageContent title={translate('Status')}>
|
||||
<PageContentBody>
|
||||
<Health />
|
||||
<DiskSpace />
|
||||
<About />
|
||||
<MoreInfo />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
function Status() {
|
||||
return (
|
||||
<PageContent title={translate('Status')}>
|
||||
<PageContentBody>
|
||||
<Health />
|
||||
<DiskSpace />
|
||||
<About />
|
||||
<MoreInfo />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Status;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { SeriesStatus } from 'Series/Series';
|
||||
|
||||
function getProgressBarKind(
|
||||
status: string,
|
||||
status: SeriesStatus,
|
||||
monitored: boolean,
|
||||
progress: number,
|
||||
isDownloading: boolean
|
||||
|
||||
@@ -49,7 +49,7 @@ class CutoffUnmetConnector extends Component {
|
||||
gotoCutoffUnmetFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate, ['episodeFileUpdated', 'episodeFileDeleted']);
|
||||
registerPagePopulator(this.repopulate, ['seriesUpdated', 'episodeFileUpdated', 'episodeFileDeleted']);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchCutoffUnmet();
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
sizes="16x16"
|
||||
href="/Content/Images/Icons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="manifest" href="/Content/manifest.json" crossorigin="use-credentials" />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
||||
@@ -47,7 +47,7 @@
|
||||
/>
|
||||
<meta
|
||||
name="msapplication-config"
|
||||
content="/Content/Images/Icons/browserconfig.xml"
|
||||
content="/Content/browserconfig.xml"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
|
||||
<meta name="description" content="Sonarr" />
|
||||
|
||||
@@ -33,7 +36,11 @@
|
||||
sizes="16x16"
|
||||
href="/Content/Images/Icons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="/Content/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
||||
@@ -45,10 +52,7 @@
|
||||
href="/favicon.ico"
|
||||
data-no-hash
|
||||
/>
|
||||
<meta
|
||||
name="msapplication-config"
|
||||
content="/Content/Images/Icons/browserconfig.xml"
|
||||
/>
|
||||
<meta name="msapplication-config" content="/Content/browserconfig.xml" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
|
||||
@@ -59,7 +63,7 @@
|
||||
body {
|
||||
background-color: var(--pageBackground);
|
||||
color: var(--textColor);
|
||||
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
||||
font-family: 'Roboto', 'open sans', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@@ -209,9 +213,7 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="sign-in">
|
||||
SIGN IN TO CONTINUE
|
||||
</div>
|
||||
<div class="sign-in">SIGN IN TO CONTINUE</div>
|
||||
|
||||
<form
|
||||
role="form"
|
||||
@@ -230,8 +232,8 @@
|
||||
pattern=".{1,}"
|
||||
required
|
||||
title="User name is required"
|
||||
autoFocus="true"
|
||||
autoCapitalize="false"
|
||||
autofocus="true"
|
||||
autocapitalize="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -282,16 +284,16 @@
|
||||
</body>
|
||||
|
||||
<script type="text/javascript">
|
||||
var yearSpan = document.getElementById("year");
|
||||
yearSpan.innerHTML = "2010-" + new Date().getFullYear();
|
||||
var yearSpan = document.getElementById('year');
|
||||
yearSpan.innerHTML = '2010-' + new Date().getFullYear();
|
||||
|
||||
var copyDiv = document.getElementById("copy");
|
||||
copyDiv.classList.remove("hidden");
|
||||
var copyDiv = document.getElementById('copy');
|
||||
copyDiv.classList.remove('hidden');
|
||||
|
||||
if (window.location.search.indexOf("loginFailed=true") > -1) {
|
||||
var loginFailedDiv = document.getElementById("login-failed");
|
||||
if (window.location.search.indexOf('loginFailed=true') > -1) {
|
||||
var loginFailedDiv = document.getElementById('login-failed');
|
||||
|
||||
loginFailedDiv.classList.remove("hidden");
|
||||
loginFailedDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
var light = {
|
||||
@@ -311,7 +313,7 @@
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
failedColor: '#f05050',
|
||||
forgotPasswordColor: '#909fa7',
|
||||
forgotPasswordAltColor: '#748690'
|
||||
forgotPasswordAltColor: '#748690',
|
||||
};
|
||||
|
||||
var dark = {
|
||||
@@ -331,21 +333,16 @@
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
failedColor: '#f05050',
|
||||
forgotPasswordColor: '#737d83',
|
||||
forgotPasswordAltColor: '#546067'
|
||||
forgotPasswordAltColor: '#546067',
|
||||
};
|
||||
|
||||
var theme = "_THEME_";
|
||||
var theme = '_THEME_';
|
||||
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
|
||||
dark :
|
||||
light;
|
||||
var finalTheme =
|
||||
theme === 'dark' || (theme === 'auto' && defaultDark) ? dark : light;
|
||||
|
||||
Object.entries(finalTheme).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--${key}`,
|
||||
value
|
||||
);
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export type CheckInputChanged = {
|
||||
export type InputChanged<T = unknown> = {
|
||||
name: string;
|
||||
value: boolean;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type CheckInputChanged = InputChanged<boolean>;
|
||||
|
||||
22
package.json
22
package.json
@@ -84,13 +84,13 @@
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/core": "7.25.2",
|
||||
"@babel/eslint-parser": "7.25.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@babel/preset-env": "7.25.3",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@types/lodash": "4.14.194",
|
||||
"@types/qs": "6.9.15",
|
||||
"@types/react-document-title": "2.0.9",
|
||||
@@ -102,11 +102,11 @@
|
||||
"@types/webpack-livereload-plugin": "^2.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.37.0",
|
||||
"core-js": "3.38.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.0",
|
||||
@@ -123,11 +123,11 @@
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.38",
|
||||
"postcss": "8.4.41",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
"postcss-nested": "6.0.1",
|
||||
"postcss-nested": "6.2.0",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"postcss-url": "10.1.3",
|
||||
"prettier": "2.8.8",
|
||||
|
||||
@@ -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")]
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,8 +2,11 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
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 +65,143 @@ 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_id_has_multi_languages_configuration()
|
||||
{
|
||||
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||
var indexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Id = 1,
|
||||
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 });
|
||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_multi_languages_from_indexer_with_id_when_indexer_id_and_name_are_set()
|
||||
{
|
||||
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||
var indexerDefinition1 = new IndexerDefinition
|
||||
{
|
||||
Id = 1,
|
||||
Name = "MyIndexer1",
|
||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||
};
|
||||
var indexerDefinition2 = new IndexerDefinition
|
||||
{
|
||||
Id = 2,
|
||||
Name = "MyIndexer2",
|
||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } }
|
||||
};
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.Get(1))
|
||||
.Returns(indexerDefinition1);
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 });
|
||||
|
||||
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle);
|
||||
_remoteEpisode.Release.IndexerId = 1;
|
||||
_remoteEpisode.Release.Indexer = "MyIndexer2";
|
||||
_remoteEpisode.Release.Title = releaseTitle;
|
||||
|
||||
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French });
|
||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_multi_languages_when_indexer_name_has_multi_languages_configuration()
|
||||
{
|
||||
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||
var indexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Id = 1,
|
||||
Name = "MyIndexer (Prowlarr)",
|
||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||
};
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.FindByName("MyIndexer (Prowlarr)"))
|
||||
.Returns(indexerDefinition);
|
||||
|
||||
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle);
|
||||
_remoteEpisode.Release.Indexer = "MyIndexer (Prowlarr)";
|
||||
_remoteEpisode.Release.Title = releaseTitle;
|
||||
|
||||
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage, Language.French });
|
||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer (Prowlarr)"), Times.Once());
|
||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
Id = 1,
|
||||
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 });
|
||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
Id = 1,
|
||||
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 });
|
||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_original_when_no_indexer_value()
|
||||
{
|
||||
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||
|
||||
_remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List<Language> { }, releaseTitle);
|
||||
_remoteEpisode.Release.Title = releaseTitle;
|
||||
|
||||
Subject.Aggregate(_remoteEpisode).Languages.Should().BeEquivalentTo(new List<Language> { _series.OriginalLanguage });
|
||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title()
|
||||
{
|
||||
|
||||
@@ -10,7 +10,6 @@ using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.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>()
|
||||
|
||||
@@ -7,6 +7,8 @@ using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.TorrentRss;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -84,6 +86,80 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||
trackedDownload.RemoteEpisode.MappedSeasonNumber.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_indexer()
|
||||
{
|
||||
var episodeHistory = new EpisodeHistory()
|
||||
{
|
||||
DownloadId = "35238",
|
||||
SourceTitle = "TV Series S01",
|
||||
SeriesId = 5,
|
||||
EpisodeId = 4,
|
||||
EventType = EpisodeHistoryEventType.Grabbed,
|
||||
};
|
||||
episodeHistory.Data.Add("indexer", "MyIndexer (Prowlarr)");
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.FindByDownloadId(It.Is<string>(sr => sr == "35238")))
|
||||
.Returns(new List<EpisodeHistory>()
|
||||
{
|
||||
episodeHistory
|
||||
});
|
||||
|
||||
var indexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Id = 1,
|
||||
Name = "MyIndexer (Prowlarr)",
|
||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||
};
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.Get(indexerDefinition.Id))
|
||||
.Returns(indexerDefinition);
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<IndexerDefinition>() { indexerDefinition });
|
||||
|
||||
var remoteEpisode = new RemoteEpisode
|
||||
{
|
||||
Series = new Series() { Id = 5 },
|
||||
Episodes = new List<Episode> { new Episode { Id = 4 } },
|
||||
ParsedEpisodeInfo = new ParsedEpisodeInfo()
|
||||
{
|
||||
SeriesTitle = "TV Series",
|
||||
SeasonNumber = 1
|
||||
},
|
||||
MappedSeasonNumber = 1
|
||||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<string>(), null))
|
||||
.Returns(remoteEpisode);
|
||||
|
||||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Title = "TV.Series.S01.MULTi.1080p.WEB.H265-RlsGroup",
|
||||
DownloadId = "35238",
|
||||
DownloadClientInfo = new DownloadClientItemClientInfo
|
||||
{
|
||||
Protocol = client.Protocol,
|
||||
Id = client.Id,
|
||||
Name = client.Name
|
||||
}
|
||||
};
|
||||
|
||||
var trackedDownload = Subject.TrackDownload(client, item);
|
||||
|
||||
trackedDownload.Should().NotBeNull();
|
||||
trackedDownload.RemoteEpisode.Should().NotBeNull();
|
||||
trackedDownload.RemoteEpisode.Release.Should().NotBeNull();
|
||||
trackedDownload.RemoteEpisode.Release.Indexer.Should().Be("MyIndexer (Prowlarr)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_as_special_when_source_title_parsing_fails()
|
||||
{
|
||||
@@ -118,7 +194,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 +245,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 +275,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 +304,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 +334,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 +363,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 +393,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));
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.IO;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Extras.Metadata;
|
||||
using NzbDrone.Core.Extras.Metadata.Consumers.Kometa;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Kometa
|
||||
{
|
||||
[TestFixture]
|
||||
public class FindMetadataFileFixture : CoreTest<KometaMetadata>
|
||||
{
|
||||
private Series _series;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>.CreateNew()
|
||||
.With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic())
|
||||
.Build();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_null_if_filename_is_not_handled()
|
||||
{
|
||||
var path = Path.Combine(_series.Path, "file.jpg");
|
||||
|
||||
Subject.FindMetadataFile(_series, path).Should().BeNull();
|
||||
}
|
||||
|
||||
[TestCase("Season00")]
|
||||
[TestCase("Season01")]
|
||||
[TestCase("Season02")]
|
||||
public void should_return_season_image(string folder)
|
||||
{
|
||||
var path = Path.Combine(_series.Path, folder + ".jpg");
|
||||
|
||||
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage);
|
||||
}
|
||||
|
||||
[TestCase(".jpg", MetadataType.EpisodeImage)]
|
||||
public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type)
|
||||
{
|
||||
var path = Path.Combine(_series.Path, "s01e01" + extension);
|
||||
|
||||
Subject.FindMetadataFile(_series, path).Type.Should().Be(type);
|
||||
}
|
||||
|
||||
[TestCase(".jpg")]
|
||||
public void should_return_null_if_not_valid_file_for_episode(string extension)
|
||||
{
|
||||
var path = Path.Combine(_series.Path, "the.series.episode" + extension);
|
||||
|
||||
Subject.FindMetadataFile(_series, path).Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_return_metadata_if_image_file_is_a_thumb()
|
||||
{
|
||||
var path = Path.Combine(_series.Path, "the.series.s01e01.episode-thumb.jpg");
|
||||
|
||||
Subject.FindMetadataFile(_series, path).Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_series_image_for_folder_jpg_in_series_folder()
|
||||
{
|
||||
var path = Path.Combine(_series.Path, "poster.jpg");
|
||||
|
||||
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 White‘s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||
[TestCase("Betty White’s Off Their Rockers", "Betty+Whites+Off+Their+Rockers")]
|
||||
public void should_replace_some_special_characters(string input, string expected)
|
||||
{
|
||||
Subject.SceneTitles = new List<string> { input };
|
||||
|
||||
44
src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs
Normal file
44
src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Indexers
|
||||
{
|
||||
[TestFixture]
|
||||
public class IndexerRepositoryFixture : DbTest<IndexerRepository, IndexerDefinition>
|
||||
{
|
||||
private void GivenIndexers()
|
||||
{
|
||||
var indexers = Builder<IndexerDefinition>.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(c => c.Id = 0)
|
||||
.TheFirst(1)
|
||||
.With(x => x.Name = "MyIndexer (Prowlarr)")
|
||||
.TheNext(1)
|
||||
.With(x => x.Name = "My Second Indexer (Prowlarr)")
|
||||
.BuildList();
|
||||
|
||||
Subject.InsertMany(indexers);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_finds_with_name()
|
||||
{
|
||||
GivenIndexers();
|
||||
var found = Subject.FindByName("MyIndexer (Prowlarr)");
|
||||
found.Should().NotBeNull();
|
||||
found.Name.Should().Be("MyIndexer (Prowlarr)");
|
||||
found.Id.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_find_with_incorrect_case_name()
|
||||
{
|
||||
GivenIndexers();
|
||||
var found = Subject.FindByName("myindexer (prowlarr)");
|
||||
found.Should().BeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -39,22 +39,29 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
Path = @"C:\Test\30 Rock\30.rock.s01e01.avi",
|
||||
Episodes = episodes,
|
||||
Series = _series,
|
||||
Quality = new QualityModel(Quality.HDTV720p)
|
||||
Quality = new QualityModel(Quality.HDTV720p),
|
||||
};
|
||||
}
|
||||
|
||||
private void GivenRuntime(int seconds)
|
||||
{
|
||||
var runtime = new TimeSpan(0, 0, seconds);
|
||||
|
||||
Mocker.GetMock<IVideoFileInfoReader>()
|
||||
.Setup(s => s.GetRunTime(It.IsAny<string>()))
|
||||
.Returns(new TimeSpan(0, 0, seconds));
|
||||
.Returns(runtime);
|
||||
|
||||
_localEpisode.MediaInfo = Builder<MediaInfoModel>.CreateNew().With(m => m.RunTime = runtime).Build();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_season_zero()
|
||||
{
|
||||
_localEpisode.Episodes[0].SeasonNumber = 0;
|
||||
ShouldBeNotSample();
|
||||
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -62,7 +69,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
{
|
||||
_localEpisode.Path = @"C:\Test\some.show.s01e01.flv";
|
||||
|
||||
ShouldBeNotSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
|
||||
Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
@@ -72,7 +81,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
{
|
||||
_localEpisode.Path = @"C:\Test\some.show.s01e01.strm";
|
||||
|
||||
ShouldBeNotSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
|
||||
Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
@@ -94,7 +105,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
{
|
||||
GivenRuntime(60);
|
||||
|
||||
ShouldBeSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.Sample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -102,7 +115,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
{
|
||||
GivenRuntime(600);
|
||||
|
||||
ShouldBeNotSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -111,7 +126,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
_series.Runtime = 6;
|
||||
GivenRuntime(299);
|
||||
|
||||
ShouldBeNotSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -120,7 +137,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
_series.Runtime = 2;
|
||||
GivenRuntime(60);
|
||||
|
||||
ShouldBeNotSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -129,7 +148,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
_series.Runtime = 2;
|
||||
GivenRuntime(10);
|
||||
|
||||
ShouldBeSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.Sample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -152,7 +173,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
GivenRuntime(600);
|
||||
_series.SeriesType = SeriesTypes.Daily;
|
||||
_localEpisode.Episodes[0].SeasonNumber = 0;
|
||||
ShouldBeNotSample();
|
||||
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -161,21 +185,33 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
_series.SeriesType = SeriesTypes.Anime;
|
||||
_localEpisode.Episodes[0].SeasonNumber = 0;
|
||||
|
||||
ShouldBeNotSample();
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
}
|
||||
|
||||
private void ShouldBeSample()
|
||||
[Test]
|
||||
public void should_use_runtime_from_media_info()
|
||||
{
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.Sample);
|
||||
GivenRuntime(120);
|
||||
|
||||
_localEpisode.Series.Runtime = 30;
|
||||
_localEpisode.Episodes.First().Runtime = 30;
|
||||
|
||||
Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample);
|
||||
|
||||
Mocker.GetMock<IVideoFileInfoReader>().Verify(v => v.GetRunTime(It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
|
||||
private void ShouldBeNotSample()
|
||||
[Test]
|
||||
public void should_use_runtime_from_episode_over_series()
|
||||
{
|
||||
Subject.IsSample(_localEpisode.Series,
|
||||
_localEpisode.Path,
|
||||
_localEpisode.IsSpecial).Should().Be(DetectSampleResult.NotSample);
|
||||
GivenRuntime(120);
|
||||
|
||||
_localEpisode.Series.Runtime = 5;
|
||||
_localEpisode.Episodes.First().Runtime = 30;
|
||||
|
||||
Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user