1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-27 17:54:34 -04:00

Compare commits

...

42 Commits

Author SHA1 Message Date
Stevie Robinson
ff1987be84 New: Remove defunct Boxcar notifications
(cherry picked from commit c6ad2396bb98dc8eb1ad47bf5d066b227a47f8b5)

Closes #9451
2023-11-25 22:29:33 +02:00
Bogdan
cb08c0767d Switch assignment to operator 2023-11-22 21:50:29 +02:00
Bogdan
5f1d7ddc11 Improve discover movies sorting by release dates 2023-11-21 22:45:23 +02:00
Bogdan
0ba3c08ea6 Small UI fixes to discover movies 2023-11-21 22:37:36 +02:00
Bogdan
6b9a378eaf Fixed: Minimum refresh interval for import list presets 2023-11-21 19:52:58 +02:00
Bogdan
b4562e6236 Fixed: Movie grabbed history on interactive search 2023-11-21 03:45:19 +02:00
Bogdan
bbffff78ed Fixed: Disable swipe on movie details when a modal is open 2023-11-20 22:16:01 +02:00
Bogdan
740f0f1e5f Cleanup unused logic in Movie Details 2023-11-20 21:37:13 +02:00
Bogdan
45b38b44c1 Fixed: Interactive search modal on mobile 2023-11-20 07:52:48 +02:00
Mark McDowall
318d59bb99 Fixed force saving provider triggering testing
(cherry picked from commit 65cb1ccafd54479fa3fca1f1eaa4b96222b0176b)
2023-11-20 07:37:42 +02:00
Bogdan
ed54d071c4 Fixed: Wrap long lines in media info popup 2023-11-20 07:06:45 +02:00
Bogdan
cff15de4fc Translate custom format score for history actions 2023-11-20 05:48:02 +02:00
Bogdan
88c0e24c58 Fixed: Clear movie search results when navigating to another page
(cherry picked from commit 67dc8987970aa2a9eade48c02ae72be1851fa196)
2023-11-19 21:10:43 -06:00
Qstick
8e0645670b New: Rework Movie Details view 2023-11-19 21:10:43 -06:00
Qstick
40eeb31a21 New: Move Interactive search to toolbar 2023-11-19 21:10:43 -06:00
Qstick
3e534cf8bf Bump version to 5.2.3 2023-11-19 18:31:23 -06:00
Bogdan
c96b3c4b0b Fixed: Autotagging Genres are case insensitive
(cherry picked from commit 87ecbf39c1c0cc8a3a3f4ee1d1878b34ea49f6b8)

Closes #9436
2023-11-20 00:59:06 +02:00
Mark McDowall
78bc9f9b4b Fixed: Saving indexer, download client, etc settings
(cherry picked from commit 804a5921b3b620e2407d5d6a7fd69fb1fd9b0cbf)
2023-11-20 00:53:45 +02:00
Servarr
b737f05a83 Automated API Docs update 2023-11-19 22:40:17 +02:00
Bogdan
8d96fd2387 More cleanup appName tokens 2023-11-19 22:35:57 +02:00
servarr[bot]
6c487ead00 Fixed: Disable SSL when using the removed SslCertHash configuration (#9426)
(cherry picked from commit d95660d3c78d1ee11a7966d58e78a82a8df01393)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2023-11-19 21:46:06 +02:00
Bogdan
22e3cf844c New: Confirmation before clearing blocklist
(cherry picked from commit 366d8a4e78e024bc045e4791d3a51af391f4e95a)

Closes #9429
Fixes #9401
2023-11-19 21:17:00 +02:00
Bogdan
14b4b5e122 Cleanup appName tokens
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>

Closes #9430
2023-11-19 21:14:48 +02:00
Mark McDowall
10f5f3c5c8 New: Require password confirmation when setting or changing password
(cherry picked from commit b248163df598dc611ee919d525eb7357256d73d5)
2023-11-19 21:10:52 +02:00
Bogdan
c687b552f0 Bump version to 5.2.2 2023-11-19 07:06:29 +02:00
Stevie Robinson
92c8c8a7f5 Translate System pages
(cherry picked from commit 93e8ff0ac7610fa8739f2e577ece98c2c06c8881)

Closes #8852
2023-11-18 23:44:43 +02:00
Stevie Robinson
86a16c3c0c Fix translation token for Update Check
(cherry picked from commit cd1d8a3ff04b0d06f6fa7dfeb7a7ab6dd88b36b3)

Closes #9341
2023-11-18 23:44:41 +02:00
Weblate
e8c280db34 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Francisco Cachado <franciscomcachado@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bowsefather <husseinali39@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2023-11-18 03:21:28 +02:00
Nesego
ea65e2174c Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (1336 of 1336 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
2023-11-18 03:01:28 +02:00
Weblate
4aa2466693 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Francisco Cachado <franciscomcachado@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bowsefather <husseinali39@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2023-11-18 02:59:39 +02:00
Bogdan
4df0f0f721 Revert translation keys 2023-11-18 02:49:58 +02:00
Mark McDowall
d7bee375e8 Use named tokens for backend translations
(cherry picked from commit 11f96c31048c2d1aafca0c91736d439f7f9a95a8)
2023-11-18 02:37:33 +02:00
Bogdan
906295466d Use variable for App name in translations
Co-authored-by: Qstick <qstick@gmail.com>
2023-11-18 02:37:33 +02:00
Mark McDowall
f86060eca2 Don't retest unchanged providers
New: Don't retest connections, indexers, download clients, etc if re-saved with the exact same settings

Closes #9411
Fixes ##9397

(cherry picked from commit 71fd09f162b2880c461e03cba4317c34ee3203dc)
2023-11-17 16:27:42 +02:00
Mark McDowall
bf9a0b62f2 Rename 'ReturnUrl' to 'returnUrl' for forms auth redirection
(cherry picked from commit 812712e2843a738054c065a6d5c1b7c81c5f8e7b)
2023-11-17 03:30:03 +02:00
Bogdan
ccc62f0450 Fixed: Enforce validation warnings when testing providers
(cherry picked from commit c3b4126d0c4f449a41e2cf7ea438b20e25370995)
2023-11-17 02:40:39 +02:00
bakerboy448
524657ad78 Improve sample detection logging (#9405)
Sonarr 6181
2023-11-15 19:38:58 +02:00
Qstick
7a394ff864 Update menu background on dark theme 2023-11-14 20:57:26 -06:00
Bogdan
d8862eedd3 Fixed: Refresh discovery movies state on refresh lists 2023-11-14 20:36:12 +02:00
Bogdan
71f700e240 Fixed: (HDBits) Add labels for categories, codecs and mediums 2023-11-14 17:13:34 +02:00
Bogdan
ae5dd84e0a Fixed: (HDBits) Increase search limit to 100 releases 2023-11-14 15:13:45 +02:00
Bogdan
17b398cf62 Bump version to 5.2.1 2023-11-12 16:52:53 +02:00
148 changed files with 3732 additions and 3462 deletions

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.2.0'
majorVersion: '5.2.3'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'

View File

@@ -36,6 +36,7 @@ class Blocklist extends Component {
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
isConfirmClearModalOpen: false,
items: props.items
};
}
@@ -90,6 +91,19 @@ class Blocklist extends Component {
this.setState({ isConfirmRemoveModalOpen: false });
};
onClearBlocklistPress = () => {
this.setState({ isConfirmClearModalOpen: true });
};
onClearBlocklistConfirmed = () => {
this.props.onClearBlocklistPress();
this.setState({ isConfirmClearModalOpen: false });
};
onConfirmClearModalClose = () => {
this.setState({ isConfirmClearModalOpen: false });
};
//
// Render
@@ -103,7 +117,6 @@ class Blocklist extends Component {
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
@@ -111,7 +124,8 @@ class Blocklist extends Component {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
isConfirmRemoveModalOpen,
isConfirmClearModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
@@ -131,8 +145,9 @@ class Blocklist extends Component {
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
onPress={this.onClearBlocklistPress}
/>
</PageToolbarSection>
@@ -215,6 +230,16 @@ class Blocklist extends Component {
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
<ConfirmModal
isOpen={isConfirmClearModalOpen}
kind={kinds.DANGER}
title={translate('ClearBlocklist')}
message={translate('ClearBlocklistMessageText')}
confirmLabel={translate('Clear')}
onConfirm={this.onClearBlocklistConfirmed}
onCancel={this.onConfirmClearModalClose}
/>
</PageContent>
);
}

View File

@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AppUpdated', { appName: 'Radarr' })}
{translate('AppUpdated')}
</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} />
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
</div>
{

View File

@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostToBackend', { appName: 'Radarr' })}
{translate('ConnectionLostToBackend')}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostReconnect', { appName: 'Radarr' })}
{translate('ConnectionLostReconnect')}
</div>
</ModalBody>
<ModalFooter>

View File

@@ -1,9 +1,7 @@
.description {
line-height: $lineHeight;
}
.description {
margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
}
@media (min-width: 768px) {

View File

@@ -63,6 +63,12 @@
width: 1280px;
}
.extraExtraLarge {
composes: modal;
width: 1600px;
}
@media only screen and (max-width: $breakpointExtraLarge) {
.modal.extraLarge {
width: 90%;
@@ -90,7 +96,8 @@
.modal.small,
.modal.medium,
.modal.large,
.modal.extraLarge {
.modal.extraLarge,
.modal.extraExtraLarge {
max-height: 100%;
width: 100%;
height: 100% !important;

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'extraExtraLarge': string;
'extraLarge': string;
'large': string;
'medium': string;

View File

@@ -78,7 +78,8 @@ function createMapDispatchToProps(dispatch, props) {
onImportListSyncPress() {
dispatch(executeCommand({
name: commandNames.IMPORT_LIST_SYNC
name: commandNames.IMPORT_LIST_SYNC,
commandFinished: this.dispatchFetchListMovies
}));
}
};

View File

@@ -50,7 +50,7 @@ $hoverScale: 1.05;
.title {
@add-mixin truncate;
background-color: #fafbfc;
background-color: var(--movieBackgroundColor);
text-align: center;
font-size: $smallFontSize;
}

View File

@@ -92,6 +92,7 @@ class DiscoverMoviePoster extends Component {
showRelativeDates,
shortDateFormat,
timeFormat,
movieRuntimeFormat,
...otherProps
} = this.props;
@@ -110,7 +111,7 @@ class DiscoverMoviePoster extends Component {
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
<div className={styles.posterContainer} title={title}>
{
<div className={styles.editorSelect}>
<CheckInput
@@ -185,7 +186,7 @@ class DiscoverMoviePoster extends Component {
{
showTitle &&
<div className={styles.title}>
<div className={styles.title} title={title}>
{title}
</div>
}
@@ -194,6 +195,7 @@ class DiscoverMoviePoster extends Component {
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
movieRuntimeFormat={movieRuntimeFormat}
{...otherProps}
/>
@@ -236,6 +238,7 @@ DiscoverMoviePoster.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,

View File

@@ -5,9 +5,11 @@ import DiscoverMoviePoster from './DiscoverMoviePoster';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(),
( dimensions) => {
(movieRuntimeFormat, dimensions) => {
return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}

View File

@@ -1,5 +1,5 @@
.info {
background-color: #fafbfc;
background-color: var(--movieBackgroundColor);
text-align: center;
font-size: $smallFontSize;
}

View File

@@ -1,9 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TmdbRating from 'Components/TmdbRating';
import { icons } from 'Helpers/Props';
import { getMovieStatusDetails } from 'Movie/MovieStatus';
import formatRuntime from 'Utilities/Date/formatRuntime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
import styles from './DiscoverMoviePosterInfo.css';
function DiscoverMoviePosterInfo(props) {
@@ -19,12 +22,13 @@ function DiscoverMoviePosterInfo(props) {
sortKey,
showRelativeDates,
shortDateFormat,
timeFormat
timeFormat,
movieRuntimeFormat
} = props;
if (sortKey === 'status' && status) {
return (
<div className={styles.info}>
<div className={styles.info} title={translate('Status')}>
{getMovieStatusDetails(status).title}
</div>
);
@@ -32,7 +36,7 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'studio' && studio) {
return (
<div className={styles.info}>
<div className={styles.info} title={translate('Studio')}>
{studio}
</div>
);
@@ -50,8 +54,8 @@ function DiscoverMoviePosterInfo(props) {
);
return (
<div className={styles.info}>
{`In Cinemas ${inCinemasDate}`}
<div className={styles.info} title={translate('InCinemas')}>
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
</div>
);
}
@@ -68,8 +72,8 @@ function DiscoverMoviePosterInfo(props) {
);
return (
<div className={styles.info}>
{`Digital ${digitalReleaseDate}`}
<div className={styles.info} title={translate('DigitalRelease')}>
<Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate}
</div>
);
}
@@ -86,15 +90,15 @@ function DiscoverMoviePosterInfo(props) {
);
return (
<div className={styles.info}>
{`Released ${physicalReleaseDate}`}
<div className={styles.info} title={translate('PhysicalRelease')}>
<Icon name={icons.DISC} /> {physicalReleaseDate}
</div>
);
}
if (sortKey === 'certification' && certification) {
return (
<div className={styles.info}>
<div className={styles.info} title={translate('Certification')}>
{certification}
</div>
);
@@ -102,8 +106,8 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'runtime' && runtime) {
return (
<div className={styles.info}>
{formatRuntime(runtime)}
<div className={styles.info} title={translate('Runtime')}>
{formatRuntime(runtime, movieRuntimeFormat)}
</div>
);
}
@@ -111,9 +115,7 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'ratings' && ratings) {
return (
<div className={styles.info}>
<TmdbRating
ratings={ratings}
/>
<TmdbRating ratings={ratings} />
</div>
);
}
@@ -133,7 +135,8 @@ DiscoverMoviePosterInfo.propTypes = {
sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired
};
export default DiscoverMoviePosterInfo;

View File

@@ -76,6 +76,7 @@ class DiscoverMovieRow extends Component {
ratings,
popularity,
certification,
movieRuntimeFormat,
collection,
columns,
isExisting,
@@ -230,7 +231,7 @@ class DiscoverMovieRow extends Component {
key={name}
className={styles[name]}
>
{formatRuntime(runtime)}
{formatRuntime(runtime, movieRuntimeFormat)}
</VirtualTableRowCell>
);
}
@@ -397,6 +398,7 @@ DiscoverMovieRow.propTypes = {
popularity: PropTypes.number.isRequired,
certification: PropTypes.string,
collection: PropTypes.object,
movieRuntimeFormat: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired,

View File

@@ -5,9 +5,11 @@ import DiscoverMovieRow from './DiscoverMovieRow';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(),
(dimensions) => {
(movieRuntimeFormat, dimensions) => {
return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}

View File

@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod,
authenticationRequired,
username,
password
password,
passwordConfirmation
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
@@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
{translate('AuthenticationRequiredWarning')}
</Alert>
{
@@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
onChange={onInputChange}
@@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
{...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
}

View File

@@ -3,5 +3,6 @@ export const SMALL = 'small';
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];
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];

View File

@@ -3,13 +3,16 @@ import React, { Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css';
import styles from './InteractiveSearch.css';
const columns = [
{
@@ -24,23 +27,6 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'title',
label: () => translate('Title'),
@@ -84,12 +70,6 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'customFormat',
label: () => translate('Formats'),
isSortable: true,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
@@ -107,10 +87,27 @@ const columns = [
}),
isSortable: true,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
}
];
function InteractiveSearchContent(props) {
function InteractiveSearch(props) {
const {
searchPayload,
isFetching,
@@ -118,18 +115,36 @@ function InteractiveSearchContent(props) {
error,
totalReleasesCount,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
onFilterSelect,
onGrabPress
} = props;
const errorMessage = getErrorMessage(error);
const type = 'movies';
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect}
/>
</div>
{
isFetching ? <LoadingIndicator /> : null
}
@@ -203,19 +218,23 @@ function InteractiveSearchContent(props) {
);
}
InteractiveSearchContent.propTypes = {
InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default InteractiveSearchContent;
export default InteractiveSearch;

View File

@@ -2,10 +2,11 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearchContent from './InteractiveSearchContent';
import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) {
return createSelector(
@@ -29,8 +30,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(releaseActions.fetchReleases(payload));
},
dispatchClearReleases(payload) {
dispatch(releaseActions.clearReleases(payload));
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
onSortPress(sortKey, sortDirection) {
@@ -38,8 +43,7 @@ function createMapDispatchToProps(dispatch, props) {
},
onFilterSelect(selectedFilterKey) {
const action = releaseActions.setReleasesFilter;
dispatch(action({ selectedFilterKey }));
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
},
onGrabPress(payload) {
@@ -48,7 +52,7 @@ function createMapDispatchToProps(dispatch, props) {
};
}
class InteractiveSearchContentConnector extends Component {
class InteractiveSearchConnector extends Component {
//
// Lifecycle
@@ -57,7 +61,8 @@ class InteractiveSearchContentConnector extends Component {
const {
searchPayload,
isPopulated,
dispatchFetchReleases
dispatchFetchReleases,
dispatchFetchMovieHistory
} = this.props;
// If search results are not yet isPopulated fetch them,
@@ -65,6 +70,12 @@ class InteractiveSearchContentConnector extends Component {
if (!isPopulated) {
dispatchFetchReleases(searchPayload);
}
dispatchFetchMovieHistory(searchPayload);
}
componentWillUnmount() {
this.props.dispatchClearMovieHistory();
}
//
@@ -73,24 +84,26 @@ class InteractiveSearchContentConnector extends Component {
render() {
const {
dispatchFetchReleases,
dispatchClearReleases,
dispatchFetchMovieHistory,
dispatchClearMovieHistory,
...otherProps
} = this.props;
return (
<InteractiveSearchContent
<InteractiveSearch
{...otherProps}
/>
);
}
}
InteractiveSearchContentConnector.propTypes = {
InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);

View File

@@ -4,7 +4,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import { align } from 'Helpers/Props';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import styles from './InteractiveSearchContent.css';
import styles from './InteractiveSearch.css';
function InteractiveSearchFilterMenu(props) {
const {

View File

@@ -1,23 +1,25 @@
.cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.protocol {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.titleContent {
display: flex;
align-items: center;
justify-content: space-between;
word-break: break-all;
}
.indexer {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 85px;
}
.quality,
.customFormat,
.languages {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.languages {
@@ -25,7 +27,7 @@
}
.customFormatScore {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
font-weight: bold;
@@ -33,31 +35,28 @@
}
.rejected,
.indexerFlags {
composes: cell;
.indexerFlags,
.download {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.age,
.size {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.peers {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}
.titleContent {
overflow-wrap: break-word;
}
.history {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}
@@ -67,7 +66,7 @@
}
.download {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}

View File

@@ -3,8 +3,6 @@
interface CssExports {
'age': string;
'blocklist': string;
'cell': string;
'customFormat': string;
'customFormatScore': string;
'download': string;
'downloadIcon': string;

View File

@@ -133,9 +133,9 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
longDateFormat,
timeFormat,
grabError,
historyGrabbedData,
historyFailedData,
blocklistData,
historyGrabbedData = {} as MovieHistory,
historyFailedData = {} as MovieHistory,
blocklistData = {} as MovieBlocklist,
searchPayload,
onGrabPress,
} = props;
@@ -199,53 +199,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
{formatAge(age, ageHours, ageMinutes)}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.RIGHT}
/>
) : null}
</TableRowCell>
<TableRowCell>
<div className={styles.titleContent}>
<Link to={infoUrl} title={title}>
@@ -316,10 +269,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
<MovieQuality quality={quality} />
</TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
@@ -348,6 +297,53 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
) : null}
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<ConfirmModal
isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

View File

@@ -1,11 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCastPoster extends Component {
@@ -60,7 +57,7 @@ class MovieCastPoster extends Component {
images,
posterWidth,
posterHeight,
importListId
importList
} = this.props;
const {
@@ -69,36 +66,31 @@ class MovieCastPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
{
importListId > 0 ?
<IconButton
className={styles.action}
name={icons.EDIT}
title={translate('EditPerson')}
onPress={this.onEditImportListPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div className={styles.controls}>
<MonitorToggleButton
className={styles.action}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<div
style={elementStyle}
@@ -148,12 +140,8 @@ MovieCastPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
importListId: PropTypes.number.isRequired,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
MovieCastPoster.defaultProps = {
importListId: 0
};
export default MovieCastPoster;

View File

@@ -1,11 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCrewPoster extends Component {
@@ -60,7 +57,7 @@ class MovieCrewPoster extends Component {
images,
posterWidth,
posterHeight,
importListId
importList
} = this.props;
const {
@@ -69,36 +66,31 @@ class MovieCrewPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
{
importListId > 0 ?
<IconButton
className={styles.action}
name={icons.EDIT}
title={translate('EditPerson')}
onPress={this.onEditImportListPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div className={styles.controls}>
<MonitorToggleButton
className={styles.action}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<div
style={elementStyle}
@@ -148,12 +140,8 @@ MovieCrewPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
importListId: PropTypes.number.isRequired,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
MovieCrewPoster.defaultProps = {
importListId: 0
};
export default MovieCrewPoster;

View File

@@ -18,7 +18,7 @@ function createMapStateToProps() {
}, []);
return {
items: crew
items: _.uniqBy(crew, 'personName')
};
}
);

View File

@@ -1,17 +1,13 @@
$hoverScale: 1.05;
.content {
border-radius: '5px';
transition: all 200ms ease-in;
&:hover {
z-index: 2;
box-shadow: 0 0 12px var(--black);
transition: all 200ms ease-in;
.controls {
opacity: 0.9;
transition: opacity 200ms linear 150ms;
}
}
}
@@ -50,22 +46,18 @@ $hoverScale: 1.05;
.controls {
position: absolute;
bottom: 10px;
left: 10px;
top: 10px;
z-index: 3;
border-radius: 4px;
background-color: #707070;
color: var(--white);
font-size: $smallFontSize;
opacity: 0;
transition: opacity 0;
}
.action {
composes: button from '~Components/Link/IconButton.css';
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 25px;
color: var(--white);
&:hover {
color: var(--radarrYellow);
color: var(--iconButtonHoverLightColor);
}
}

View File

@@ -1,11 +1,19 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
function createMapStateToProps() {
return createMovieCreditListSelector();
return createSelector(
createMovieCreditListSelector(),
(importList) => {
return {
importList
};
}
);
}
const mapDispatchToProps = {
@@ -20,7 +28,7 @@ class MovieCreditPosterConnector extends Component {
// Listeners
onImportListSelect = () => {
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', presetName: undefined });
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
};

View File

@@ -2,6 +2,10 @@
flex: 1 0 auto;
}
.movie {
padding: 10px;
}
.container {
padding: 10px;
}

View File

@@ -3,6 +3,7 @@
interface CssExports {
'container': string;
'grid': string;
'movie': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,12 +1,15 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import dimensions from 'Styles/Variables/dimensions';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
@@ -65,39 +68,11 @@ class MovieCreditPosters extends Component {
};
this._isInitialized = false;
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
if (this._grid &&
(prevState.width !== width ||
prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
@@ -117,7 +92,10 @@ class MovieCreditPosters extends Component {
});
};
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
//
// Render
render() {
const {
items,
itemComponent
@@ -126,99 +104,44 @@ class MovieCreditPosters extends Component {
const {
posterWidth,
posterHeight,
columnCount
} = this.state;
const movieIdx = rowIndex * columnCount + columnIndex;
const movie = items[movieIdx];
if (!movie) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={style}
>
<MovieCreditPosterConnector
key={movie.order}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={movie.personTmdbId}
personName={movie.personName}
job={movie.job}
character={movie.character}
images={movie.images}
/>
</div>
);
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
const rowCount = Math.ceil(items.length / columnCount);
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
<div className={styles.sliderContainer}>
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={3}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}
>
{items.map((credit) => (
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
<MovieCreditPosterConnector
key={credit.id}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
job={credit.job}
character={credit.character}
images={credit.images}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}
}

View File

@@ -1,3 +0,0 @@
.alternateTitle {
white-space: nowrap;
}

View File

@@ -1,28 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './MovieAlternateTitles.css';
function MovieAlternateTitles({ alternateTitles }) {
return (
<ul>
{
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
return (
<li
key={alternateTitle}
className={styles.alternateTitle}
>
{alternateTitle}
</li>
);
})
}
</ul>
);
}
MovieAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default MovieAlternateTitles;

View File

@@ -5,7 +5,7 @@
.header {
position: relative;
width: 100%;
height: 375px;
height: 425px;
}
.errorMessage {
@@ -39,10 +39,11 @@
}
.poster {
z-index: 2;
flex-shrink: 0;
margin-right: 35px;
width: 217px;
height: 319px;
width: 250px;
height: 368px;
}
.info {

View File

@@ -1,9 +1,9 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel';
@@ -23,12 +23,11 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -38,8 +37,6 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import formatRuntime from 'Utilities/Date/formatRuntime';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
@@ -57,14 +54,6 @@ function getFanartUrl(images) {
return _.find(images, { coverType: 'fanart' })?.url;
}
function getExpandedState(newState) {
return {
allExpanded: newState.allSelected,
allCollapsed: newState.allUnselected,
expandedState: newState.selectedState
};
}
class MovieDetails extends Component {
//
@@ -78,10 +67,8 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {},
selectedTabIndex: 0,
isInteractiveSearchModalOpen: false,
isMovieHistoryModalOpen: false,
overviewHeight: 0,
titleWidth: 0
};
@@ -114,10 +101,6 @@ class MovieDetails extends Component {
this.setState({ isOrganizeModalOpen: false });
};
onManageEpisodesPress = () => {
this.setState({ isManageEpisodesOpen: true });
};
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
};
@@ -134,6 +117,14 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false });
};
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
};
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditMovieModalOpen: false,
@@ -145,27 +136,12 @@ class MovieDetails extends Component {
this.setState({ isDeleteMovieModalOpen: false });
};
onExpandAllPress = () => {
const {
allExpanded,
expandedState
} = this.state;
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
onMovieHistoryPress = () => {
this.setState({ isMovieHistoryModalOpen: true });
};
onExpandPress = (seasonNumber, isExpanded) => {
this.setState((state) => {
const convertedState = {
allSelected: state.allExpanded,
allUnselected: state.allCollapsed,
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], seasonNumber, isExpanded, false);
return getExpandedState(newState);
});
onMovieHistoryModalClose = () => {
this.setState({ isMovieHistoryModalOpen: false });
};
onMeasure = ({ height }) => {
@@ -204,7 +180,12 @@ class MovieDetails extends Component {
if (
touchStart < 50 ||
this.props.isSidebarVisible ||
this.state.isEventModalOpen
this.state.isOrganizeModalOpen ||
this.state.isEditMovieModalOpen ||
this.state.isDeleteMovieModalOpen ||
this.state.isInteractiveImportModalOpen ||
this.state.isInteractiveSearchModalOpen ||
this.state.isMovieHistoryModalOpen
) {
return;
}
@@ -239,10 +220,6 @@ class MovieDetails extends Component {
}
};
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index });
};
//
// Render
@@ -295,9 +272,10 @@ class MovieDetails extends Component {
isEditMovieModalOpen,
isDeleteMovieModalOpen,
isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
isMovieHistoryModalOpen,
overviewHeight,
titleWidth,
selectedTabIndex
titleWidth
} = this.state;
const fanartUrl = getFanartUrl(images);
@@ -324,6 +302,14 @@ class MovieDetails extends Component {
onPress={onSearchPress}
/>
<PageToolbarButton
label={translate('InteractiveSearch')}
iconName={icons.INTERACTIVE}
isSpinning={isSearching}
title={undefined}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@@ -339,6 +325,12 @@ class MovieDetails extends Component {
onPress={this.onInteractiveImportPress}
/>
<PageToolbarButton
label={translate('History')}
iconName={icons.HISTORY}
onPress={this.onMovieHistoryPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@@ -654,101 +646,33 @@ class MovieDetails extends Component {
null
}
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
<TabList
className={styles.tabList}
>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('History')}
</Tab>
<FieldSet legend={translate('Files')}>
<MovieFileEditorTable
movieId={id}
/>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Search')}
</Tab>
<ExtraFileTable
movieId={id}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Files')}
</Tab>
<FieldSet legend={translate('Cast')}>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Titles')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Cast')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Crew')}
</Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector />
</div>
}
</TabList>
<TabPanel>
<MovieHistoryTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<InteractiveSearchTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieFileEditorTable
movieId={id}
/>
<ExtraFileTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieTitlesTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
<TabPanel>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
</Tabs>
<FieldSet legend={translate('Crew')}>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<FieldSet legend={translate('Titles')}>
<MovieTitlesTable
movieId={id}
/>
</FieldSet>
</div>
<OrganizePreviewModalConnector
@@ -764,6 +688,12 @@ class MovieDetails extends Component {
onDeleteMoviePress={this.onDeleteMoviePress}
/>
<MovieHistoryModal
isOpen={isMovieHistoryModalOpen}
movieId={id}
onModalClose={this.onMovieHistoryModalClose}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
movieId={id}
@@ -780,6 +710,12 @@ class MovieDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody>
</PageContent>
);

View File

@@ -11,7 +11,6 @@ import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
@@ -182,12 +181,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearMovieFiles() {
dispatch(clearMovieFiles());
},
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
dispatchFetchMovieCredits({ movieId }) {
dispatch(fetchMovieCredits({ movieId }));
},
@@ -283,7 +276,6 @@ class MovieDetailsConnector extends Component {
this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieBlocklist({ movieId });
this.props.dispatchFetchMovieHistory({ movieId });
this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId });
@@ -294,7 +286,6 @@ class MovieDetailsConnector extends Component {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieBlocklist();
this.props.dispatchClearMovieFiles();
this.props.dispatchClearMovieHistory();
this.props.dispatchClearExtraFiles();
this.props.dispatchClearMovieCredits();
this.props.dispatchClearQueueDetails();
@@ -351,8 +342,6 @@ MovieDetailsConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
dispatchFetchMovieFiles: PropTypes.func.isRequired,
dispatchClearMovieFiles: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired,
dispatchFetchExtraFiles: PropTypes.func.isRequired,
dispatchClearExtraFiles: PropTypes.func.isRequired,
dispatchFetchMovieCredits: PropTypes.func.isRequired,

View File

@@ -1,5 +1,4 @@
.container {
margin-top: 20px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);

View File

@@ -1,5 +1,6 @@
import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) {
const {
@@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
} = props;
return (
<MovieTitlesTableContentConnector
{...otherProps}
/>
<div className={styles.container}>
<MovieTitlesTableContentConnector
{...otherProps}
/>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
function MovieHistoryModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
size={sizes.EXTRA_LARGE}
>
<MovieHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModal;

View File

@@ -0,0 +1,142 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
label: () => translate('Actions'),
isVisible: true
}
];
class MovieHistoryModalContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress,
onModalClose
} = this.props;
const hasItems = !!items.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('History')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
}
{
isPopulated && !hasItems && !error &&
<div>{translate('NoHistory')}</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
MovieHistoryModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModalContent;

View File

@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryTableContent from './MovieHistoryTableContent';
import { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryModalContent from './MovieHistoryModalContent';
function createMapStateToProps() {
return createSelector(
@@ -15,10 +15,29 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchMovieHistory,
clearMovieHistory,
movieHistoryMarkAsFailed
};
class MovieHistoryTableContentConnector extends Component {
class MovieHistoryModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
movieId
} = this.props;
this.props.fetchMovieHistory({
movieId
});
}
componentWillUnmount() {
this.props.clearMovieHistory();
}
//
// Listeners
@@ -39,7 +58,7 @@ class MovieHistoryTableContentConnector extends Component {
render() {
return (
<MovieHistoryTableContent
<MovieHistoryModalContent
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
@@ -47,9 +66,11 @@ class MovieHistoryTableContentConnector extends Component {
}
}
MovieHistoryTableContentConnector.propTypes = {
MovieHistoryModalContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
fetchMovieHistory: PropTypes.func.isRequired,
clearMovieHistory: PropTypes.func.isRequired,
movieHistoryMarkAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryTableContentConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);

View File

@@ -1,22 +0,0 @@
import React from 'react';
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
import styles from './MovieHistoryTable.css';
function MovieHistoryTable(props) {
const {
...otherProps
} = props;
return (
<div className={styles.container}>
<MovieHistoryTableContentConnector
{...otherProps}
/>
</div>
);
}
MovieHistoryTable.propTypes = {
};
export default MovieHistoryTable;

View File

@@ -1,128 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
import styles from './MovieHistoryTableContent.css';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: 'Custom format score'
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
isVisible: true
}
];
class MovieHistoryTableContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress
} = this.props;
const hasItems = !!items.length;
return (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>
{translate('UnableToLoadHistory')}
</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>
{translate('NoHistory')}
</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
MovieHistoryTableContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default MovieHistoryTableContent;

View File

@@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieInteractiveSearchModalContent from './MovieInteractiveSearchModalContent';
function MovieInteractiveSearchModal(props) {
const {
isOpen,
movieId,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
size={sizes.EXTRA_EXTRA_LARGE}
>
<MovieInteractiveSearchModalContent
movieId={movieId}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModal;

View File

@@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import MovieInteractiveSearchModal from './MovieInteractiveSearchModal';
function createMapDispatchToProps(dispatch, props) {
return {
dispatchCancelFetchReleases() {
dispatch(cancelFetchReleases());
},
dispatchClearReleases() {
dispatch(clearReleases());
},
onModalClose() {
dispatch(cancelFetchReleases());
dispatch(clearReleases());
props.onModalClose();
}
};
}
class MovieInteractiveSearchModalConnector extends Component {
//
// Lifecycle
componentWillUnmount() {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearReleases();
}
//
// Render
render() {
const {
dispatchCancelFetchReleases,
dispatchClearReleases,
...otherProps
} = this.props;
return (
<MovieInteractiveSearchModal
{...otherProps}
/>
);
}
}
MovieInteractiveSearchModalConnector.propTypes = {
...MovieInteractiveSearchModal.propTypes,
dispatchCancelFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(MovieInteractiveSearchModalConnector);

View File

@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
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 { scrollDirections } from 'Helpers/Props';
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
import translate from 'Utilities/String/translate';
function MovieInteractiveSearchModalContent(props) {
const {
movieId,
onModalClose
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('InteractiveSearchModalHeader')}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
<InteractiveSearchConnector
searchPayload={{ movieId }}
/>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
MovieInteractiveSearchModalContent.propTypes = {
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModalContent;

View File

@@ -1,5 +1,4 @@
.container {
margin-top: 20px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);

View File

@@ -124,6 +124,7 @@ class SecuritySettings extends Component {
authenticationRequired,
username,
password,
passwordConfirmation,
apiKey,
certificateValidation
} = settings;
@@ -139,8 +140,8 @@ class SecuritySettings extends Component {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={translate('AuthenticationRequiredWarning')}
onChange={onInputChange}
{...authenticationMethod}
/>
@@ -193,6 +194,21 @@ class SecuritySettings extends Component {
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
{...passwordConfirmation}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>{translate('ApiKey')}</FormLabel>

View File

@@ -83,7 +83,7 @@ function UpdateSettings(props) {
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Radarr' }) : undefined}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>

View File

@@ -122,7 +122,7 @@ export default {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = payload.presetName ?? payload.implementationName;
selectedSchema.implementationName = payload.implementationName;
selectedSchema.minRefreshInterval = payload.minRefreshInterval;
selectedSchema.minRefreshInterval = selectedSchema.minRefreshInterval ?? payload.minRefreshInterval;
selectedSchema.minimumAvailability = 'released';
selectedSchema.rootFolderPath = '';

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import moment from 'moment/moment';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
@@ -9,7 +10,7 @@ import getNewMovie from 'Utilities/Movie/getNewMovie';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import translate from 'Utilities/String/translate';
import { removeItem, set, updateItem } from './baseActions';
import { removeItem, set, update, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
@@ -219,6 +220,42 @@ export const defaultState = {
const { ratings = {} } = item;
return ratings.tmdb? ratings.tmdb.value : 0;
},
inCinemas: function(item, direction) {
if (item.inCinemas) {
return moment(item.inCinemas).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
},
physicalRelease: function(item, direction) {
if (item.physicalRelease) {
return moment(item.physicalRelease).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
},
digitalRelease: function(item, direction) {
if (item.digitalRelease) {
return moment(item.digitalRelease).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
}
},
@@ -507,11 +544,11 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
// set an Id so the selectors and updaters done blow up.
// set an ID so the selectors and updaters done blow up.
data = data.map((movie) => ({ ...movie, id: movie.tmdbId }));
dispatch(batchActions([
...data.map((movie) => updateItem({ section, ...movie })),
update({ section, data }),
set({
section,

View File

@@ -91,7 +91,7 @@ export const defaultState = {
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, {
name: icons.SCORE,
title: 'Custom format score'
title: () => translate('CustomFormatScore')
}),
isVisible: false
},

View File

@@ -1,33 +0,0 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
function createMovieCollectionListSelector() {
return createSelector(
(state, { tmdbId }) => tmdbId,
(state) => state.settings.importLists.items,
(tmdbId, importLists) => {
const importListIds = _.reduce(importLists, (acc, list) => {
if (list.implementation === 'TMDbCollectionImport') {
const collectionIdField = list.fields.find((field) => {
return field.name === 'collectionId';
});
if (collectionIdField && parseInt(collectionIdField.value) === tmdbId) {
acc.push(list);
return acc;
}
}
return acc;
}, []);
if (importListIds.length === 0) {
return undefined;
}
return importListIds[0];
}
);
}
export default createMovieCollectionListSelector;

View File

@@ -21,15 +21,11 @@ function createMovieCreditListSelector() {
return acc;
}, []);
let importListId = 0;
if (importListIds.length > 0) {
importListId = importListIds[0].id;
if (importListIds.length === 0) {
return undefined;
}
return {
importListId
};
return importListIds[0];
}
);
}

View File

@@ -70,7 +70,7 @@ module.exports = {
// Toolbar
toolbarColor: '#e1e2e3',
toolbarBackgroundColor: '#262626',
toolbarMenuItemBackgroundColor: '#606060',
toolbarMenuItemBackgroundColor: '#303030',
toolbarMenuItemHoverBackgroundColor: '#515151',
toolbarLabelColor: '#e1e2e3',

View File

@@ -116,6 +116,7 @@ class BackupRow extends Component {
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RestoreBackup')}
name={icons.RESTORE}
onPress={this.onRestorePress}
/>
@@ -138,7 +139,9 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', { name })}
message={translate('DeleteBackupMessageText', {
name
})}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose}

View File

@@ -109,7 +109,7 @@ class Backups extends Component {
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadBackups')}
{translate('BackupsLoadError')}
</Alert>
}

View File

@@ -146,7 +146,9 @@ class RestoreBackupModalContent extends Component {
<ModalBody>
{
!!id && translate('WouldYouLikeToRestoreBackup', { name })
!!id && translate('WouldYouLikeToRestoreBackup', {
name
})
}
{

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -67,7 +67,7 @@ class LogFiles extends Component {
/>
<PageToolbarButton
label={translate('Delete')}
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={deleteFilesExecuting}
onPress={onDeleteFilesPress}
@@ -77,13 +77,15 @@ class LogFiles extends Component {
<PageContentBody>
<Alert>
<div>
Log files are located in: {location}
{translate('LogFilesLocation', {
location
})}
</div>
{
currentLogView === 'Log Files' &&
<div>
{translate('TheLogLevelDefault')} <Link to="/settings/general">{translate('GeneralSettings')}</Link>
<InlineMarkdown data={translate('TheLogLevelDefault')} />
</div>
}
</Alert>

View File

@@ -4,6 +4,7 @@ import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';
import translate from 'Utilities/String/translate';
class LogsNavMenu extends Component {
@@ -50,13 +51,13 @@ class LogsNavMenu extends Component {
<MenuItem
to={'/system/logs/files'}
>
Log Files
{translate('LogFiles')}
</MenuItem>
<MenuItem
to={'/system/logs/files/update'}
>
Updater Log Files
{translate('UpdaterLogFiles')}
</MenuItem>
</MenuContent>
</Menu>

View File

@@ -45,7 +45,14 @@ class About extends Component {
packageVersion &&
<DescriptionListItem
title={translate('PackageVersion')}
data={(packageAuthor ? <span> {packageVersion} {' by '} <InlineMarkdown data={packageAuthor} /> </span> : packageVersion)}
data={(packageAuthor ?
<InlineMarkdown data={translate('PackageVersionInfo', {
packageVersion,
packageAuthor
})}
/> :
packageVersion
)}
/>
}

View File

@@ -153,7 +153,7 @@ class Health extends Component {
{
!healthIssues &&
<div className={styles.healthOk}>
{translate('HealthNoIssues')}
{translate('NoIssuesWithYourConfiguration')}
</div>
}

View File

@@ -26,7 +26,7 @@ class MoreInfo extends Component {
{translate('Wiki')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://wiki.servarr.com/radarr">{translate('Wiki')}</Link>
<Link to="https://wiki.servarr.com/radarr">wiki.servarr.com/radarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>

View File

@@ -44,7 +44,7 @@ class Updates extends Component {
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterPrefix = translate('UnableToUpdateRadarrDirectly');
const externalUpdaterPrefix = translate('UpdateRadarrDirectlyLoadError');
const externalUpdaterMessages = {
external: translate('ExternalUpdater'),
apt: translate('AptUpdater'),
@@ -176,7 +176,7 @@ class Updates extends Component {
kind={kinds.INVERSE}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
Previously Installed
{translate('PreviouslyInstalled')}
</Label> :
null
}
@@ -213,14 +213,14 @@ class Updates extends Component {
{
!!updatesError &&
<div>
Failed to fetch updates
{translate('FailedToFetchUpdates')}
</div>
}
{
!!generalSettingsError &&
<div>
Failed to update settings
{translate('FailedToUpdateSettings')}
</div>
}
</PageContentBody>

View File

@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate(
key: string,
tokens?: Record<string, string | number | boolean>
tokens: Record<string, string | number | boolean> = {}
) {
const translation = translations[key] || key;
if (tokens) {
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
tokens.appName = 'Radarr';
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
return translation;
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}

View File

@@ -24,15 +24,17 @@ namespace NzbDrone.Core.Test.Localization
[Test]
public void should_get_string_in_dictionary_if_lang_exists_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("UiLanguage");
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("UI Language");
}
[Test]
public void should_get_string_in_default_language_dictionary_if_no_lang_country_code_exists_and_string_exists()
public void should_get_string_in_french()
{
var localizedString = Subject.GetLocalizedString("UiLanguage", "fr_fr");
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.French);
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("Langue de l'IU");
@@ -40,19 +42,10 @@ namespace NzbDrone.Core.Test.Localization
}
[Test]
public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists()
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("UiLanguage", "an");
localizedString.Should().Be("UI Language");
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("UiLanguage", "");
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns(0);
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("UI Language");
}
@@ -60,7 +53,7 @@ namespace NzbDrone.Core.Test.Localization
[Test]
public void should_return_argument_if_string_doesnt_exists()
{
var localizedString = Subject.GetLocalizedString("badString", "en");
var localizedString = Subject.GetLocalizedString("badString");
localizedString.Should().Be("badString");
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Validation;
@@ -27,7 +28,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
{
return movie.MovieMetadata.Value.Genres.Any(genre => Value.Contains(genre));
return movie?.MovieMetadata?.Value?.Genres.Any(genre => Value.ContainsIgnoreCase(genre)) ?? false;
}
public override NzbDroneValidationResult Validate()

View File

@@ -323,6 +323,20 @@ namespace NzbDrone.Core.Configuration
}
}
public void MigrateConfigFile()
{
if (!File.Exists(_configFile))
{
return;
}
// If SSL is enabled and a cert hash is still in the config file disable SSL
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
{
SetValue("EnableSsl", false);
}
}
private void DeleteOldValues()
{
var xDoc = LoadConfigFile();
@@ -394,6 +408,7 @@ namespace NzbDrone.Core.Configuration
public void HandleAsync(ApplicationStartedEvent message)
{
MigrateConfigFile();
EnsureDefaultConfigFile();
DeleteOldValues();
}

View File

@@ -1,4 +1,5 @@
using NLog;
using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Lifecycle;
@@ -28,7 +29,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
_logger.Warn("Please update your API key to be at least {0} characters long. You can do this via settings or the config file", MinimumLength);
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage"), MinimumLength), "#invalid-api-key");
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary<string, object> { { "length", MinimumLength } }), "#invalid-api-key");
}
return new HealthCheck(GetType());

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Download;
@@ -42,8 +43,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
_logger.Debug(ex, "Unable to communicate with {0}", downloadClient.Definition.Name);
var message = string.Format(_localizationService.GetLocalizedString("DownloadClientCheckUnableToCommunicateMessage"), downloadClient.Definition.Name);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"{message} {ex.Message}", "#unable-to-communicate-with-download-client");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("DownloadClientCheckUnableToCommunicateMessage", new Dictionary<string, object>
{
{ "downloadClientName", downloadClient.Definition.Name },
{ "errorMessage", ex.Message }
}),
"#unable-to-communicate-with-download-client");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
@@ -44,7 +45,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("DownloadClientRemovesCompletedDownloadsHealthCheckMessage"), clientName, "Radarr"),
_localizationService.GetLocalizedString("DownloadClientRemovesCompletedDownloadsHealthCheckMessage", new Dictionary<string, object>
{
{ "downloadClientName", clientName }
}),
"#download-client-removes-completed-downloads");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NLog;
@@ -52,7 +53,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
foreach (var folder in folders)
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot"), client.Definition.Name, folder.FullPath), "#downloads-in-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot", new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#downloads-in-root-folder");
}
}
catch (DownloadClientException ex)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
@@ -43,7 +44,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (status.SortingMode.IsNotNullOrWhiteSpace())
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientSortingCheckMessage"), clientName, status.SortingMode), "#download-folder-and-library-folder-not-different-folders");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("DownloadClientSortingCheckMessage", new Dictionary<string, object>
{
{ "downloadClientName", clientName },
{ "sortingMode", status.SortingMode }
}),
"#download-folder-and-library-folder-not-different-folders");
}
}
catch (DownloadClientException ex)

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download;
@@ -40,7 +41,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("DownloadClientStatusCheckAllClientMessage"), "#download-clients-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download-clients-are-unavailable-due-to-failures");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "downloadClientNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#download-clients-are-unavailable-due-to-failures");
}
}
}

View File

@@ -54,11 +54,23 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (missingRootFolders.Count == 1)
{
var missingRootFolder = missingRootFolders.First();
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ImportListMissingRoot"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), "#import-list-missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ImportListMissingRoot", new Dictionary<string, object>
{
{ "rootFolderInfo", FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value) }
}),
"#import-list-missing-root-folder");
}
var message = string.Format(_localizationService.GetLocalizedString("ImportListMultipleMissingRoots"), string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))));
return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#import-list-missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ImportListMultipleMissingRoots", new Dictionary<string, object>
{
{ "rootFoldersInfo", string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))) }
}),
"#import-list-missing-root-folder");
}
return new HealthCheck(GetType());

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.ImportLists;
@@ -40,7 +41,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("ImportListStatusCheckAllClientMessage"), "#lists-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ImportListStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.ImportList.Definition.Name))), "#lists-are-unavailable-due-to-failures");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("ImportListStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "importListNames", string.Join(", ", backOffProviders.Select(v => v.ImportList.Definition.Name)) }
}),
"#import-lists-are-unavailable-due-to-failures");
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
@@ -35,7 +36,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage"), string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray())),
_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray()) }
}),
"#invalid-indexer-download-client-setting");
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
@@ -41,8 +42,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerJackettAll"),
string.Join(", ", jackettAllProviders.Select(i => i.Name))),
_localizationService.GetLocalizedString("IndexerJackettAll", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", jackettAllProviders.Select(i => i.Name)) }
}),
"#jackett-all-endpoint-used");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
@@ -50,8 +51,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#indexers-are-unavailable-due-to-failures");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
@@ -48,8 +49,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#indexers-are-unavailable-due-to-failures");
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Localization;
@@ -45,7 +46,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage", new Dictionary<string, object>
{
{ "notificationNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#notifications-are-unavailable-due-to-failures");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
@@ -40,7 +41,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (!addresses.Any())
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname), "#proxy-failed-resolve-ip");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage", new Dictionary<string, object>
{
{ "proxyHostName", _configService.ProxyHostname }
}),
"#proxy-failed-resolve-ip");
}
var request = _cloudRequestBuilder.Create()
@@ -55,13 +62,27 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode), "#proxy-failed-test");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage", new Dictionary<string, object>
{
{ "statusCode", response.StatusCode }
}),
"#proxy-failed-test");
}
}
catch (Exception ex)
{
_logger.Error(ex, "Proxy Health Check failed");
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url), "#proxy-failed-test");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage", new Dictionary<string, object>
{
{ "url", request.Url }
}),
"#proxy-failed-test");
}
return new HealthCheck(GetType());

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
@@ -31,7 +32,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (!_diskProvider.FolderWritable(recycleBin))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RecycleBinUnableToWriteHealthCheck"), recycleBin), "#cannot-write-recycle-bin");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RecycleBinUnableToWriteHealthCheck", new Dictionary<string, object>
{
{ "path", recycleBin }
}),
"#cannot-write-recycle-bin");
}
return new HealthCheck(GetType());

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NLog;
@@ -69,30 +70,92 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckWrongOSPath", new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath },
{ "osName", _osInfo.Name }
}),
"#bad-remote-path-mapping");
}
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckBadDockerPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckBadDockerPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath },
{ "osName", _osInfo.Name }
}),
"#docker-bad-remote-path-mapping");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-download-client-settings");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckLocalWrongOSPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath },
{ "osName", _osInfo.Name }
}),
"#bad-download-client-settings");
}
if (!_diskProvider.FolderExists(folder.FullPath))
{
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDockerFolderMissing"), client.Definition.Name, folder.FullPath), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckDockerFolderMissing",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#docker-bad-remote-path-mapping");
}
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalFolderMissing"), client.Definition.Name, folder.FullPath), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckLocalFolderMissing",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#bad-remote-path-mapping");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckGenericPermissions"), client.Definition.Name, folder.FullPath), "#permissions-error");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckGenericPermissions",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#permissions-error");
}
}
}
@@ -130,12 +193,28 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (_diskProvider.FileExists(moviePath))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDownloadPermissions"), moviePath), "#permissions-error");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckDownloadPermissions",
new Dictionary<string, object>
{
{ "path", moviePath }
}),
"#permissions-error");
}
// If the file doesn't exist but MovieInfo is not null then the message is coming from
// ImportApprovedMovies and the file must have been removed part way through processing
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFileRemoved"), moviePath), "#remote-path-file-removed");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFileRemoved",
new Dictionary<string, object>
{
{ "path", moviePath }
}),
"#remote-path-file-removed");
}
// If the previous case did not match then the failure occured in DownloadedMovieImportService,
@@ -157,42 +236,118 @@ namespace NzbDrone.Core.HealthCheck.Checks
// that the user realises something is wrong.
if (dlpath.IsNullOrWhiteSpace())
{
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("RemotePathMappingCheckImportFailed"), "#remote-path-import-failed");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RemotePathMappingCheckImportFailed"),
"#remote-path-import-failed");
}
if (!dlpath.IsPathValid(PathValidationType.CurrentOs))
{
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesWrongOSPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}),
"#bad-remote-path-mapping");
}
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesBadDockerPath"), client.Definition.Name, dlpath, _osInfo.Name), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesBadDockerPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}),
"#docker-bad-remote-path-mapping");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesLocalWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-download-client-settings");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesLocalWrongOSPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}),
"#bad-download-client-settings");
}
if (_diskProvider.FolderExists(dlpath))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFolderPermissions"), dlpath), "#permissions-error");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFolderPermissions",
new Dictionary<string, object>
{
{ "path", dlpath }
}),
"#permissions-error");
}
// if it's a remote client/docker, likely missing path mappings
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFolderPermissions"), client.Definition.Name, dlpath), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFolderPermissions",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath }
}),
"#docker-bad-remote-path-mapping");
}
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckRemoteDownloadClient"), client.Definition.Name, dlpath), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckRemoteDownloadClient",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}), "#bad-remote-path-mapping");
}
// path mappings shouldn't be needed locally so probably a permissions issue
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesGenericPermissions"), client.Definition.Name, dlpath), "#permissions-error");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesGenericPermissions",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath }
}),
"#permissions-error");
}
catch (DownloadClientException ex)
{

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Localization;
@@ -32,10 +33,22 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (deletedMovie.Count == 1)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemovedMovieCheckSingleMessage"), movieText), "#movie-was-removed-from-tmdb");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RemovedMovieCheckSingleMessage", new Dictionary<string, object>
{
{ "movie", movieText }
}),
"#movie-was-removed-from-tmdb");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemovedMovieCheckMultipleMessage"), movieText), "#movie-was-removed-from-tmdb");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RemovedMovieCheckMultipleMessage", new Dictionary<string, object>
{
{ "movies", movieText }
}),
"#movie-was-removed-from-tmdb");
}
public bool ShouldCheckOnEvent(MoviesDeletedEvent message)

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
@@ -40,11 +41,26 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
if (missingRootFolders.Count == 1)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RootFolderCheckSingleMessage"), missingRootFolders.First()), "#missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RootFolderCheckSingleMessage",
new Dictionary<string, object>
{
{ "rootFolderPath", missingRootFolders.First() }
}),
"#missing-root-folder");
}
var message = string.Format(_localizationService.GetLocalizedString("RootFolderCheckMultipleMessage"), string.Join(" | ", missingRootFolders));
return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RootFolderCheckMultipleMessage",
new Dictionary<string, object>
{
{ "rootFolderPaths", string.Join(" | ", missingRootFolders) }
}),
"#missing-root-folder");
}
return new HealthCheck(GetType());

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
@@ -47,7 +48,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupTranslocationMessage"), startupFolder),
_localizationService.GetLocalizedString(
"UpdateCheckStartupTranslocationMessage",
new Dictionary<string, object>
{
{ "startupFolder", startupFolder }
}),
"#cannot-install-update-because-startup-folder-is-in-an-app-translocation-folder.");
}
@@ -55,7 +61,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupNotWritableMessage"), startupFolder, Environment.UserName),
_localizationService.GetLocalizedString(
"UpdateCheckStartupNotWritableMessage",
new Dictionary<string, object>
{
{ "startupFolder", startupFolder },
{ "userName", Environment.UserName }
}),
"#cannot-install-update-because-startup-folder-is-not-writable-by-the-user");
}
@@ -63,7 +75,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
string.Format(_localizationService.GetLocalizedString("UpdateCheckUINotWritableMessage"), uiFolder, Environment.UserName),
_localizationService.GetLocalizedString(
"UpdateCheckUINotWritableMessage",
new Dictionary<string, object>
{
{ "uiFolder", uiFolder },
{ "userName", Environment.UserName }
}),
"#cannot-install-update-because-ui-folder-is-not-writable-by-the-user");
}
}

View File

@@ -38,6 +38,7 @@ namespace NzbDrone.Core.ImportLists.RSSImport
EnableAuto = true,
QualityProfileId = 1,
Implementation = GetType().Name,
MinRefreshInterval = MinRefreshInterval,
Settings = new RSSImportSettings { Link = "https://rss.imdb.com/list/YOURLISTID" },
};
yield return new ImportListDefinition
@@ -47,6 +48,7 @@ namespace NzbDrone.Core.ImportLists.RSSImport
EnableAuto = true,
QualityProfileId = 1,
Implementation = GetType().Name,
MinRefreshInterval = MinRefreshInterval,
Settings = new RSSImportSettings { Link = "https://rss.imdb.com/user/IMDBUSERID/watchlist" },
};
}

View File

@@ -47,6 +47,7 @@ namespace NzbDrone.Core.ImportLists.RadarrList2.IMDbList
EnableAuto = true,
QualityProfileId = 1,
Implementation = GetType().Name,
MinRefreshInterval = MinRefreshInterval,
Settings = new IMDbListSettings { ListId = "top250" },
};
yield return new ImportListDefinition
@@ -56,6 +57,7 @@ namespace NzbDrone.Core.ImportLists.RadarrList2.IMDbList
EnableAuto = true,
QualityProfileId = 1,
Implementation = GetType().Name,
MinRefreshInterval = MinRefreshInterval,
Settings = new IMDbListSettings { ListId = "popular" },
};
}

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Core.Indexers.HDBits
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override bool SupportsRss => true;
public override bool SupportsSearch => true;
public override int PageSize => 30;
public override int PageSize => 100;
public HDBits(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Indexers.HDBits
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new HDBitsRequestGenerator() { Settings = Settings };
return new HDBitsRequestGenerator { Settings = Settings };
}
public override IParseIndexerResponse GetParser()

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Indexers.HDBits
@@ -7,19 +8,15 @@ namespace NzbDrone.Core.Indexers.HDBits
{
[JsonProperty(Required = Required.Always)]
public string Username { get; set; }
[JsonProperty(Required = Required.Always)]
public string Passkey { get; set; }
public string Hash { get; set; }
public string Search { get; set; }
public int[] Category { get; set; }
public int[] Codec { get; set; }
public int[] Medium { get; set; }
public IEnumerable<int> Category { get; set; }
public IEnumerable<int> Codec { get; set; }
public IEnumerable<int> Medium { get; set; }
public int? Origin { get; set; }
[JsonProperty(PropertyName = "imdb")]
@@ -33,6 +30,7 @@ namespace NzbDrone.Core.Indexers.HDBits
[JsonProperty(PropertyName = "snatched_only")]
public bool? SnatchedOnly { get; set; }
public int? Limit { get; set; }
public int? Page { get; set; }

View File

@@ -39,8 +39,7 @@ namespace NzbDrone.Core.Indexers.HDBits
jsonResponse.Message ?? string.Empty);
}
var responseData = jsonResponse.Data as JArray;
if (responseData == null)
if (jsonResponse.Data is not JArray responseData)
{
throw new IndexerException(indexerResponse,
"Indexer API call response missing result data");
@@ -51,7 +50,7 @@ namespace NzbDrone.Core.Indexers.HDBits
foreach (var result in queryResults)
{
var id = result.Id;
var internalRelease = result.TypeOrigin == 1 ? true : false;
var internalRelease = result.TypeOrigin == 1;
IndexerFlags flags = 0;
@@ -65,9 +64,9 @@ namespace NzbDrone.Core.Indexers.HDBits
flags |= IndexerFlags.G_Internal;
}
torrentInfos.Add(new HDBitsInfo()
torrentInfos.Add(new HDBitsInfo
{
Guid = string.Format("HDBits-{0}", id),
Guid = $"HDBits-{id}",
Title = result.Name,
Size = result.Size,
InfoHash = result.Hash,

View File

@@ -45,8 +45,9 @@ namespace NzbDrone.Core.Indexers.HDBits
if (imdbId != 0)
{
query.ImdbInfo = query.ImdbInfo ?? new ImdbInfo();
query.ImdbInfo ??= new ImdbInfo();
query.ImdbInfo.Id = imdbId;
return true;
}
@@ -74,6 +75,8 @@ namespace NzbDrone.Core.Indexers.HDBits
query.Codec = Settings.Codecs.ToArray();
query.Medium = Settings.Mediums.ToArray();
query.Limit = 100;
request.SetContent(query.ToJson());
request.ContentSummary = query.ToJson(Formatting.None);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
@@ -20,41 +21,41 @@ namespace NzbDrone.Core.Indexers.HDBits
public class HDBitsSettings : ITorrentIndexerSettings
{
private static readonly HDBitsSettingsValidator Validator = new HDBitsSettingsValidator();
private static readonly HDBitsSettingsValidator Validator = new ();
public HDBitsSettings()
{
BaseUrl = "https://hdbits.org";
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS;
Categories = new int[] { (int)HdBitsCategory.Movie };
Codecs = System.Array.Empty<int>();
Mediums = System.Array.Empty<int>();
Categories = new[] { (int)HdBitsCategory.Movie };
Codecs = Array.Empty<int>();
Mediums = Array.Empty<int>();
MultiLanguages = new List<int>();
RequiredFlags = new List<int>();
}
[FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(1, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
public string BaseUrl { get; set; }
[FieldDefinition(4, Label = "Categories", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsCategory), HelpText = "Options: Movie, TV, Documentary, Music, Sport, Audio, XXX, MiscDemo. If unspecified, all options are used.")]
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptions = typeof(HdBitsCategory), HelpText = "If unspecified, all options are used.")]
public IEnumerable<int> Categories { get; set; }
[FieldDefinition(5, Label = "Codecs", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "Options: h264, Mpeg2, VC1, Xvid. If unspecified, all options are used.")]
[FieldDefinition(4, Label = "Codecs", Type = FieldType.Select, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "If unspecified, all options are used.")]
public IEnumerable<int> Codecs { get; set; }
[FieldDefinition(6, Label = "Mediums", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "Options: BluRay, Encode, Capture, Remux, WebDL. If unspecified, all options are used.")]
[FieldDefinition(5, Label = "Mediums", Type = FieldType.Select, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "If unspecified, all options are used.")]
public IEnumerable<int> Mediums { get; set; }
[FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
public IEnumerable<int> MultiLanguages { get; set; }
[FieldDefinition(7, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
public int MinimumSeeders { get; set; }
@@ -62,7 +63,7 @@ namespace NzbDrone.Core.Indexers.HDBits
public IEnumerable<int> RequiredFlags { get; set; }
[FieldDefinition(9)]
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
public SeedCriteriaSettings SeedCriteria { get; set; } = new ();
public NzbDroneValidationResult Validate()
{
@@ -72,31 +73,49 @@ namespace NzbDrone.Core.Indexers.HDBits
public enum HdBitsCategory
{
[FieldOption("Movie")]
Movie = 1,
[FieldOption("TV")]
Tv = 2,
[FieldOption("Documentary")]
Documentary = 3,
[FieldOption("Music")]
Music = 4,
[FieldOption("Sport")]
Sport = 5,
[FieldOption("Audio Track")]
Audio = 6,
[FieldOption("XXX")]
Xxx = 7,
[FieldOption("Misc/Demo")]
MiscDemo = 8
}
public enum HdBitsCodec
{
[FieldOption("H.264")]
H264 = 1,
[FieldOption("MPEG-2")]
Mpeg2 = 2,
[FieldOption("VC-1")]
Vc1 = 3,
[FieldOption("XviD")]
Xvid = 4,
[FieldOption("HEVC")]
HEVC = 5
}
public enum HdBitsMedium
{
[FieldOption("Blu-ray/HD DVD")]
Bluray = 1,
[FieldOption("Encode")]
Encode = 3,
[FieldOption("Capture")]
Capture = 4,
[FieldOption("Remux")]
Remux = 5,
[FieldOption("WEB-DL")]
WebDl = 6
}
}

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