mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-14 20:55:01 -04:00
Compare commits
75 Commits
v4.6.0.743
...
v4.7.0.758
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fd4e41c85 | ||
|
|
8984fd735b | ||
|
|
3321123043 | ||
|
|
23a13b5c23 | ||
|
|
304a07e23f | ||
|
|
1e0ec4aefb | ||
|
|
5c46c75ce7 | ||
|
|
1c26dd4aca | ||
|
|
6fae00f51c | ||
|
|
653ef0a501 | ||
|
|
e606ff05a4 | ||
|
|
dd3ac26604 | ||
|
|
122d0056ea | ||
|
|
fe41aada06 | ||
|
|
bd1844030d | ||
|
|
148ee5983d | ||
|
|
372d15ecf3 | ||
|
|
b95431500d | ||
|
|
3da72f54ef | ||
|
|
7cfff20cad | ||
|
|
d3895dec8f | ||
|
|
690bab3264 | ||
|
|
986128e100 | ||
|
|
dda0885f91 | ||
|
|
7e218a886d | ||
|
|
77cde138dc | ||
|
|
239109e3dd | ||
|
|
4804eb0769 | ||
|
|
60a55dfdac | ||
|
|
cd82865303 | ||
|
|
439adb4ac6 | ||
|
|
fd0ff78791 | ||
|
|
cbae355402 | ||
|
|
fed98a648f | ||
|
|
e1c5656cff | ||
|
|
e3f88e1711 | ||
|
|
8fd267580a | ||
|
|
8974aa823b | ||
|
|
41492efd2e | ||
|
|
d008768fff | ||
|
|
cb21fe535d | ||
|
|
4cce2727e2 | ||
|
|
b1ff82da37 | ||
|
|
c5266152c5 | ||
|
|
783878be1e | ||
|
|
0cbfb4ca65 | ||
|
|
c22c9400c2 | ||
|
|
0288c4b704 | ||
|
|
e4429d2919 | ||
|
|
7052a7a5ec | ||
|
|
b38912851b | ||
|
|
1354c2c337 | ||
|
|
e259235df6 | ||
|
|
0cc1fe8308 | ||
|
|
f4fe18a440 | ||
|
|
eeed935e3a | ||
|
|
1b3701371a | ||
|
|
d56f3ec2e7 | ||
|
|
e7e3aac971 | ||
|
|
d2cb36c88a | ||
|
|
2fe28cb1dc | ||
|
|
5d65b4cae4 | ||
|
|
b0f56e2840 | ||
|
|
5593837482 | ||
|
|
8231290c7b | ||
|
|
0c1b88c60a | ||
|
|
0b8478e4a1 | ||
|
|
69e09c8687 | ||
|
|
3f2ea49023 | ||
|
|
32f09633e9 | ||
|
|
3542b263c7 | ||
|
|
d5cc84d8c8 | ||
|
|
c0790060fb | ||
|
|
5ec7e86488 | ||
|
|
b8abafd72f |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -76,7 +76,7 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
|
||||
description: Trace logs are generally required for all bug reports
|
||||
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
||||
options:
|
||||
- label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue.
|
||||
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
||||
required: true
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.6.0'
|
||||
majorVersion: '4.7.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
@@ -363,7 +363,7 @@ stages:
|
||||
- bash: |
|
||||
echo "Uploading source maps to sentry"
|
||||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
RELEASENAME="${RADARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
|
||||
RELEASENAME="Radarr@${RADARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
|
||||
sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}"
|
||||
sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
|
||||
sentry-cli releases set-commits --auto "${RELEASENAME}"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -156,16 +157,16 @@ class Blocklist extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadBlocklist')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoHistoryBlocklist')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -209,7 +210,7 @@ class Blocklist extends Component {
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('RemoveSelected')}
|
||||
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist')}
|
||||
message={translate('RemoveSelectedItemBlocklistMessageText')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
@@ -83,9 +84,9 @@ class History extends Component {
|
||||
|
||||
{
|
||||
!isFetchingAny && hasError &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadHistory')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -93,9 +94,9 @@ class History extends Component {
|
||||
// wait for the episodes to populate because they are never coming.
|
||||
|
||||
isPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -12,7 +13,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -231,17 +232,17 @@ class Queue extends Component {
|
||||
|
||||
{
|
||||
!isRefreshing && hasError ?
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('FailedToLoadQueue')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('QueueIsEmpty')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -133,7 +133,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { reduce } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
@@ -105,9 +107,9 @@ class ImportMovie extends Component {
|
||||
|
||||
{
|
||||
!rootFoldersFetching && !!rootFoldersError ?
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -116,9 +118,9 @@ class ImportMovie extends Component {
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersPopulated &&
|
||||
!unmappedFolders.length ?
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('AllMoviesInPathHaveBeenImported', [path])}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -92,9 +92,9 @@ class ImportMovieSelectFolder extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</div> :
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AgendaConnector from './Agenda/AgendaConnector';
|
||||
import * as calendarViews from './calendarViews';
|
||||
@@ -31,9 +33,9 @@ class Calendar extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadTheCalendar')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ function createMapStateToProps() {
|
||||
qualityProfileId: collection.qualityProfileId,
|
||||
minimumAvailability: collection.minimumAvailability,
|
||||
searchForMovie: collection.searchOnAdd,
|
||||
tags: []
|
||||
tags: collection.tags || []
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -9,7 +10,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import styles from 'Movie/Index/MovieIndex.css';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -313,9 +314,9 @@ class Collection extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadCollections')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -2,17 +2,12 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createCollectionSelector(),
|
||||
createAllMoviesSelector(),
|
||||
(
|
||||
collection,
|
||||
allMovies
|
||||
) => {
|
||||
(collection) => {
|
||||
// If a movie is deleted this selector may fire before the parent
|
||||
// selecors, which will result in an undefined movie, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
@@ -22,21 +17,11 @@ function createMapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
let allGenres = [];
|
||||
let libraryMovies = 0;
|
||||
|
||||
collection.movies.forEach((movie) => {
|
||||
allGenres = allGenres.concat(movie.genres);
|
||||
|
||||
if (allMovies.find((libraryMovie) => libraryMovie.tmdbId === movie.tmdbId)) {
|
||||
libraryMovies++;
|
||||
}
|
||||
});
|
||||
const allGenres = collection.movies.flatMap((movie) => movie.genres);
|
||||
|
||||
return {
|
||||
...collection,
|
||||
genres: Array.from(new Set(allGenres)).slice(0, 3),
|
||||
missingMovies: collection.movies.length - libraryMovies
|
||||
genres: Array.from(new Set(allGenres)).slice(0, 3)
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -50,6 +50,7 @@ class EditCollectionModalContent extends Component {
|
||||
minimumAvailability,
|
||||
// Id,
|
||||
rootFolderPath,
|
||||
tags,
|
||||
searchOnAdd
|
||||
} = item;
|
||||
|
||||
@@ -126,6 +127,17 @@ class EditCollectionModalContent extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={onInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ function createMapStateToProps() {
|
||||
qualityProfileId: collection.qualityProfileId,
|
||||
minimumAvailability: collection.minimumAvailability,
|
||||
rootFolderPath: collection.rootFolderPath,
|
||||
tags: collection.tags,
|
||||
searchOnAdd: collection.searchOnAdd
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ function CollectionSortMenu(props) {
|
||||
>
|
||||
{translate('Title')}
|
||||
</SortMenuItem>
|
||||
<SortMenuItem
|
||||
name="missingMovies"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Missing')}
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
);
|
||||
|
||||
@@ -580,7 +580,7 @@ EnhancedSelectInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
disabledClassName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -63,6 +63,7 @@ function ProviderFieldFormGroup(props) {
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
helpTextWarning,
|
||||
helpLink,
|
||||
placeholder,
|
||||
value,
|
||||
@@ -96,6 +97,7 @@ function ProviderFieldFormGroup(props) {
|
||||
name={name}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
helpTextWarning={helpTextWarning}
|
||||
helpLink={helpLink}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
@@ -122,6 +124,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
helpText: PropTypes.string,
|
||||
helpTextWarning: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
|
||||
@@ -19,6 +19,8 @@ function createCleanMovieSelector() {
|
||||
year,
|
||||
images,
|
||||
alternateTitles = [],
|
||||
tmdbId,
|
||||
imdbId,
|
||||
tags = []
|
||||
} = movie;
|
||||
|
||||
@@ -29,6 +31,8 @@ function createCleanMovieSelector() {
|
||||
year,
|
||||
images,
|
||||
alternateTitles,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
firstCharacter: title.charAt(0).toLowerCase(),
|
||||
tags: tags.reduce((acc, id) => {
|
||||
const matchingTag = allTags.find((tag) => tag.id === id);
|
||||
|
||||
@@ -12,6 +12,8 @@ function MovieSearchResult(props) {
|
||||
year,
|
||||
images,
|
||||
alternateTitles,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
tags
|
||||
} = props;
|
||||
|
||||
@@ -47,6 +49,22 @@ function MovieSearchResult(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'tmdbId' && tmdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
TmdbId: {tmdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'imdbId' && imdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
ImdbId: {imdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
tag ?
|
||||
<div className={styles.tagContainer}>
|
||||
@@ -69,6 +87,8 @@ MovieSearchResult.propTypes = {
|
||||
year: PropTypes.number.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tmdbId: PropTypes.number,
|
||||
imdbId: PropTypes.string,
|
||||
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
match: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ const fuseOptions = {
|
||||
keys: [
|
||||
'title',
|
||||
'alternateTitles.title',
|
||||
'tmdbId',
|
||||
'imdbId',
|
||||
'tags.label'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function PageSectionContent(props) {
|
||||
const {
|
||||
@@ -17,7 +19,7 @@ function PageSectionContent(props) {
|
||||
);
|
||||
} else if (!isFetching && !!error) {
|
||||
return (
|
||||
<div>{errorMessage}</div>
|
||||
<Alert kind={kinds.DANGER}>{errorMessage}</Alert>
|
||||
);
|
||||
} else if (isPopulated && !error) {
|
||||
return (
|
||||
|
||||
@@ -16,6 +16,46 @@
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
color: var(--white);
|
||||
transition: width 0.6s ease;
|
||||
|
||||
&.default {
|
||||
background-color: var(--darkGray);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: var(--dangerColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: var(--successColor);
|
||||
}
|
||||
|
||||
&.purple {
|
||||
background-color: var(--purple);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: var(--warningColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: var(--infoColor);
|
||||
}
|
||||
|
||||
&.queue {
|
||||
background-color: var(--queueColor);
|
||||
}
|
||||
}
|
||||
|
||||
.frontTextContainer {
|
||||
@@ -45,46 +85,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.default {
|
||||
background-color: var(--darkGray);
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: var(--dangerColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--successColor);
|
||||
}
|
||||
|
||||
.purple {
|
||||
background-color: var(--purple);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: var(--warningColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: var(--infoColor);
|
||||
}
|
||||
|
||||
.queue {
|
||||
background-color: var(--queueColor);
|
||||
}
|
||||
|
||||
.small {
|
||||
height: $progressBarSmallHeight;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ function ProgressBar(props) {
|
||||
{
|
||||
showText && width ?
|
||||
<div
|
||||
className={styles.backTextContainer}
|
||||
className={classNames(styles.backTextContainer, styles[kind])}
|
||||
style={{ width: actualWidth }}
|
||||
>
|
||||
<div className={styles.backText}>
|
||||
@@ -67,7 +67,7 @@ function ProgressBar(props) {
|
||||
{
|
||||
showText ?
|
||||
<div
|
||||
className={styles.frontTextContainer}
|
||||
className={classNames(styles.frontTextContainer, styles[kind])}
|
||||
style={{ width: progressPercent }}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -10,7 +11,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import styles from 'Movie/Index/MovieIndex.css';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -369,9 +370,9 @@ class DiscoverMovie extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadMovies')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { LanguageSettingsAppState } from 'App/State/SettingsAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -86,7 +87,9 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<div>{translate('UnableToLoadLanguages')}</div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadLanguages')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -130,7 +131,9 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
|
||||
{isFetching && <LoadingIndicator />}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<div>{translate('UnableToLoadQualities')}</div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
|
||||
@@ -303,6 +303,7 @@ class MovieDetails extends Component {
|
||||
selectedTabIndex
|
||||
} = this.state;
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
||||
|
||||
return (
|
||||
@@ -361,9 +362,11 @@ class MovieDetails extends Component {
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
style={{
|
||||
backgroundImage: `url(${getFanartUrl(images)})`
|
||||
}}
|
||||
style={
|
||||
fanartUrl ?
|
||||
{ backgroundImage: `url(${fanartUrl})` } :
|
||||
null
|
||||
}
|
||||
>
|
||||
<div className={styles.backdropOverlay} />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SelectProvider } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
||||
import { RSS_SYNC } from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -20,7 +21,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import NoMovie from 'Movie/NoMovie';
|
||||
@@ -337,7 +338,9 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<div>{translate('UnableToLoadMovies')}</div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadMovies')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isLoaded ? (
|
||||
|
||||
@@ -203,7 +203,7 @@ function EditMoviesModalContent(props: EditMoviesModalContentProps) {
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button onPress={onSavePressWrapper}>
|
||||
{translate('Apply Changes')}
|
||||
{translate('ApplyChanges')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -109,9 +110,9 @@ class FileEditModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -92,9 +93,9 @@ class SelectQualityModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import RootFolderRowConnector from './RootFolderRowConnector';
|
||||
|
||||
@@ -44,9 +46,9 @@ function RootFolders(props) {
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -152,13 +152,7 @@ class CustomFormat extends Component {
|
||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCustomFormat')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('AreYouSureYouWantToDeleteFormat', [name])}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
message={translate('DeleteCustomFormatMessageText', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -41,9 +42,9 @@ class ExportCustomFormatModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadCustomFormats')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -11,7 +12,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportCustomFormatModalContent.css';
|
||||
|
||||
@@ -95,9 +96,9 @@ class ImportCustomFormatModalContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadCustomFormats')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ class Specification extends Component {
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneFormatTag')}
|
||||
title={translate('CloneCondition')}
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneSpecificationPress}
|
||||
/>
|
||||
@@ -114,14 +114,8 @@ class Specification extends Component {
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteSpecificationModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCustomFormat')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('AreYouSureYouWantToDeleteFormat', [name])}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
title={translate('DeleteCondition')}
|
||||
message={translate('DeleteConditionMessageText', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteSpecification}
|
||||
onCancel={this.onDeleteSpecificationModalClose}
|
||||
|
||||
@@ -170,7 +170,7 @@ function ManageDownloadClientsEditModalContent(
|
||||
<div>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button onPress={save}>{translate('Apply Changes')}</Button>
|
||||
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -29,9 +29,9 @@ function DownloadClientOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadDownloadClientOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -88,8 +88,8 @@ class RemotePathMapping extends Component {
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteRemotePathMappingModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteDelayProfile')}
|
||||
message={translate('AreYouSureYouWantToDeleteThisRemotePathMapping')}
|
||||
title={translate('DeleteRemotePathMapping')}
|
||||
message={translate('DeleteRemotePathMappingMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteRemotePathMapping}
|
||||
onCancel={this.onDeleteRemotePathMappingModalClose}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
@@ -123,9 +124,9 @@ class GeneralSettings extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadGeneralSettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -168,7 +168,7 @@ class SecuritySettings extends Component {
|
||||
isOpen={this.state.isConfirmApiKeyResetModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ResetAPIKey')}
|
||||
message={translate('AreYouSureYouWantToResetYourAPIKey')}
|
||||
message={translate('ResetAPIKeyMessageText')}
|
||||
confirmLabel={translate('Reset')}
|
||||
onConfirm={this.onConfirmResetApiKey}
|
||||
onCancel={this.onCloseResetApiKeyModal}
|
||||
|
||||
@@ -89,7 +89,7 @@ class ImportListExclusion extends Component {
|
||||
isOpen={this.state.isDeleteImportExclusionModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteImportListExclusion')}
|
||||
message={translate('AreYouSureYouWantToDeleteThisImportListExclusion')}
|
||||
message={translate('DeleteImportListExclusionMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteImportExclusion}
|
||||
onCancel={this.onDeleteImportExclusionModalClose}
|
||||
|
||||
@@ -3,3 +3,9 @@
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteButton': string;
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditImportListModalContent.css';
|
||||
|
||||
@@ -42,6 +43,7 @@ function EditImportListModalContent(props) {
|
||||
name,
|
||||
enabled,
|
||||
enableAuto,
|
||||
minRefreshInterval,
|
||||
monitor,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
@@ -85,6 +87,14 @@ function EditImportListModalContent(props) {
|
||||
{message.value.message}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
<Alert
|
||||
kind={kinds.INFO}
|
||||
className={styles.message}
|
||||
>
|
||||
{translate('ListWillRefreshEveryInterp', [formatShortTimeSpan(minRefreshInterval.value)])}
|
||||
</Alert>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditImportListModalConnector from './EditImportListModalConnector';
|
||||
import styles from './ImportList.css';
|
||||
@@ -56,7 +57,8 @@ class ImportList extends Component {
|
||||
id,
|
||||
name,
|
||||
enabled,
|
||||
enableAuto
|
||||
enableAuto,
|
||||
minRefreshInterval
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -96,6 +98,12 @@ class ImportList extends Component {
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
<Label kind={kinds.INFO} title='List Refresh Interval'>
|
||||
{`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditImportListModalOpen}
|
||||
@@ -122,6 +130,7 @@ ImportList.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
enableAuto: PropTypes.bool.isRequired,
|
||||
minRefreshInterval: PropTypes.string.isRequired,
|
||||
onConfirmDeleteImportList: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function ImportListOptions(props) {
|
||||
@@ -37,31 +38,14 @@ function ImportListOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadListOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
hasSettings && !isFetching && !error &&
|
||||
<Form>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ListUpdateInterval')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="importListSyncInterval"
|
||||
min={6}
|
||||
unit="hours"
|
||||
helpText={translate('ImportListSyncIntervalHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.importListSyncInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -27,9 +27,9 @@ interface ManageIndexersEditModalContentProps {
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const enableOptions = [
|
||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
||||
{ key: 'enabled', value: 'Enabled' },
|
||||
{ key: 'disabled', value: 'Disabled' },
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'enabled', value: translate('Enabled') },
|
||||
{ key: 'disabled', value: translate('Disabled') },
|
||||
];
|
||||
|
||||
function ManageIndexersEditModalContent(
|
||||
@@ -97,7 +97,7 @@ function ManageIndexersEditModalContent(
|
||||
setPriority(value);
|
||||
break;
|
||||
default:
|
||||
console.warn('EditIndexersModalContent Unknown Input');
|
||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||
}
|
||||
},
|
||||
[]
|
||||
@@ -111,7 +111,7 @@ function ManageIndexersEditModalContent(
|
||||
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableRss')}</FormLabel>
|
||||
<FormLabel>{translate('EnableRSS')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -162,13 +162,13 @@ function ManageIndexersEditModalContent(
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.selected}>
|
||||
{translate('{count} indexers selected', { count: selectedCount })}
|
||||
{translate('CountIndexersSelected', [selectedCount])}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button onPress={save}>{translate('Apply Changes')}</Button>
|
||||
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import ManageIndexersEditModal from './Edit/ManageIndexersEditModal';
|
||||
import ManageIndexersModalRow from './ManageIndexersModalRow';
|
||||
@@ -34,43 +35,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
label: translate('Name'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'implementation',
|
||||
label: 'Implementation',
|
||||
label: translate('Implementation'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'enableRss',
|
||||
label: 'Enable RSS',
|
||||
label: translate('EnableRSS'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'enableAutomaticSearch',
|
||||
label: 'Enable Automatic Search',
|
||||
label: translate('EnableAutomaticSearch'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'enableInteractiveSearch',
|
||||
label: 'Enable Interactive Search',
|
||||
label: translate('EnableInteractiveSearch'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
label: translate('Priority'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
label: translate('Tags'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
@@ -189,12 +190,12 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load import lists.');
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load indexers.');
|
||||
const anySelected = selectedCount > 0;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Manage Import Lists</ModalHeader>
|
||||
<ModalHeader>{translate('ManageIndexers')}</ModalHeader>
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
@@ -234,7 +235,7 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
isDisabled={!anySelected}
|
||||
onPress={onDeletePress}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -242,7 +243,7 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
isDisabled={!anySelected}
|
||||
onPress={onEditPress}
|
||||
>
|
||||
Edit
|
||||
{translate('Edit')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -250,11 +251,11 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
isDisabled={!anySelected}
|
||||
onPress={onTagsPress}
|
||||
>
|
||||
Set Tags
|
||||
{translate('SetTags')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>Close</Button>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<ManageIndexersEditModal
|
||||
@@ -274,9 +275,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Import List(s)"
|
||||
message={`Are you sure you want to delete ${selectedIds.length} import list(s)?`}
|
||||
confirmLabel="Delete"
|
||||
title={translate('DeleteIndexers')}
|
||||
message={translate('DeleteIndexersMessageText', [selectedIds.length])}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onDeleteModalClose}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ManageIndexersModalRow.css';
|
||||
|
||||
interface ManageIndexersModalRowProps {
|
||||
@@ -59,15 +60,15 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.enableRss}>
|
||||
{enableRss ? 'Yes' : 'No'}
|
||||
{enableRss ? translate('Yes') : translate('No')}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.enableAutomaticSearch}>
|
||||
{enableAutomaticSearch ? 'Yes' : 'No'}
|
||||
{enableAutomaticSearch ? translate('Yes') : translate('No')}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.enableInteractiveSearch}>
|
||||
{enableInteractiveSearch ? 'Yes' : 'No'}
|
||||
{enableInteractiveSearch ? translate('Yes') : translate('No')}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
|
||||
|
||||
@@ -17,6 +17,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TagsModalContent.css';
|
||||
|
||||
interface TagsModalContentProps {
|
||||
@@ -69,19 +70,19 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
}, [tags, applyTags, onApplyTagsPress]);
|
||||
|
||||
const applyTagsOptions = [
|
||||
{ key: 'add', value: 'Add' },
|
||||
{ key: 'remove', value: 'Remove' },
|
||||
{ key: 'replace', value: 'Replace' },
|
||||
{ key: 'add', value: translate('Add') },
|
||||
{ key: 'remove', value: translate('Remove') },
|
||||
{ key: 'replace', value: translate('Replace') },
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Tags</ModalHeader>
|
||||
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
@@ -92,7 +93,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Apply Tags</FormLabel>
|
||||
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -110,7 +111,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Result</FormLabel>
|
||||
<FormLabel>{translate('Result')}</FormLabel>
|
||||
|
||||
<div className={styles.result}>
|
||||
{seriesTags.map((id) => {
|
||||
@@ -127,7 +128,11 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
||||
title={
|
||||
removeTag
|
||||
? translate('RemovingTag')
|
||||
: translate('ExistingTag')
|
||||
}
|
||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
@@ -151,7 +156,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={'Adding tag'}
|
||||
title={translate('AddingTag')}
|
||||
kind={kinds.SUCCESS}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
@@ -165,10 +170,10 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||
Apply
|
||||
{translate('Apply')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function IndexerOptions(props) {
|
||||
@@ -28,9 +29,9 @@ function IndexerOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadIndexerOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import RootFoldersConnector from 'RootFolder/RootFoldersConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -72,9 +73,9 @@ class MediaManagement extends Component {
|
||||
{
|
||||
!isFetching && error ?
|
||||
<FieldSet legend={translate('NamingSettings')}>
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadMediaManagementSettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
</FieldSet> : null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -7,7 +8,7 @@ import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NamingModal from './NamingModal';
|
||||
import styles from './Naming.css';
|
||||
@@ -110,9 +111,9 @@ class Naming extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadNamingSettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -6,8 +6,9 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Alert from '../../../Components/Alert';
|
||||
|
||||
// Note: Do Not Translate Certification Countries
|
||||
|
||||
@@ -43,9 +44,9 @@ function MetadataOptions(props) {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadIndexerOptions')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -141,7 +141,7 @@ class DelayProfile extends Component {
|
||||
isOpen={this.state.isDeleteDelayProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteDelayProfile')}
|
||||
message={translate('AreYouSureYouWantToDeleteThisDelayProfile')}
|
||||
message={translate('DeleteDelayProfileMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteDelayProfile}
|
||||
onCancel={this.onDeleteDelayProfileModalClose}
|
||||
|
||||
@@ -60,17 +60,19 @@ class ResetQualityDefinitionsModalContent extends Component {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.messageContainer}>
|
||||
{translate('AreYouSureYouWantToResetQualityDefinitions')}
|
||||
{translate('ResetQualityDefinitionsMessageText')}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ResetTitles')}</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('ResetTitles')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="resetDefinitionTitles"
|
||||
value={resetDefinitionTitles}
|
||||
helpText={translate('ResetTitlesHelpText')}
|
||||
helpText={translate('ResetDefinitionTitlesHelpText')}
|
||||
onChange={this.onResetDefinitionTitlesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import TagConnector from './TagConnector';
|
||||
import styles from './Tags.css';
|
||||
@@ -14,9 +16,9 @@ function Tags(props) {
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoTagsHaveBeenAddedYet')}
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -8,7 +9,7 @@ import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import themes from 'Styles/Themes';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
@@ -87,9 +88,9 @@ class UISettings extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadUISettings')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -234,6 +234,18 @@ export const sortPredicates = {
|
||||
}
|
||||
|
||||
return padNumber(result.toString(), 2) + qualityName;
|
||||
},
|
||||
|
||||
inCinemas: function(item) {
|
||||
return item.inCinemas || '';
|
||||
},
|
||||
|
||||
physicalRelease: function(item) {
|
||||
return item.physicalRelease || '';
|
||||
},
|
||||
|
||||
digitalRelease: function(item) {
|
||||
return item.digitalRelease || '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
@@ -60,8 +60,30 @@ export const defaultState = {
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: 'All',
|
||||
label: translate('All'),
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'missing',
|
||||
label: translate('Missing'),
|
||||
filters: [
|
||||
{
|
||||
key: 'missingMovies',
|
||||
value: 0,
|
||||
type: filterTypes.GREATER_THAN
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'complete',
|
||||
label: translate('Complete'),
|
||||
filters: [
|
||||
{
|
||||
key: 'missingMovies',
|
||||
value: 0,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -8,7 +9,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BackupRow from './BackupRow';
|
||||
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
|
||||
@@ -107,16 +108,16 @@ class Backups extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadBackups')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
noBackups &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoBackupsAreAvailable')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import LogsTableRow from './LogsTableRow';
|
||||
|
||||
@@ -82,9 +83,9 @@ function LogsTable(props) {
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoEventsFound')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import LogsNavMenu from '../LogsNavMenu';
|
||||
import LogFilesTableRow from './LogFilesTableRow';
|
||||
@@ -118,9 +118,9 @@ class LogFiles extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !items.length &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoLogFiles')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
@@ -61,9 +62,9 @@ class Updates extends Component {
|
||||
|
||||
{
|
||||
noUpdates &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoUpdatesAreAvailable')}
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
25
frontend/src/Utilities/Date/formatShortTimeSpan.js
Normal file
25
frontend/src/Utilities/Date/formatShortTimeSpan.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function formatShortTimeSpan(timeSpan) {
|
||||
if (!timeSpan) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const duration = moment.duration(timeSpan);
|
||||
|
||||
const hours = Math.floor(duration.asHours());
|
||||
const minutes = Math.floor(duration.asMinutes());
|
||||
const seconds = Math.floor(duration.asSeconds());
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours} hour(s)`;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes} minute(s)`;
|
||||
}
|
||||
|
||||
return `${seconds} second(s)`;
|
||||
}
|
||||
|
||||
export default formatShortTimeSpan;
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#3a3f51" />
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#595959" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#464b51" />
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#595959" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||
|
||||
|
||||
@@ -27,19 +27,47 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
|
||||
|
||||
var schema = SchemaBuilder.ToSchema(model);
|
||||
|
||||
schema.Should().Contain(c => c.Order == 1 && c.Name == "lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop");
|
||||
schema.Should().Contain(c => c.Order == 0 && c.Name == "firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob");
|
||||
schema.Should().Contain(c => c.Order == 1 && c.Name == "lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && c.HelpTextWarning == "Mandatory Last Name" && (string)c.Value == "Poop");
|
||||
schema.Should().Contain(c => c.Order == 0 && c.Name == "firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && c.HelpTextWarning == "Mandatory First Name" && (string)c.Value == "Bob");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void schema_should_have_nested_fields()
|
||||
{
|
||||
var model = new NestedTestModel
|
||||
{
|
||||
Name =
|
||||
{
|
||||
FirstName = "Bob",
|
||||
LastName = "Poop"
|
||||
}
|
||||
};
|
||||
|
||||
var schema = SchemaBuilder.ToSchema(model);
|
||||
|
||||
schema.Should().Contain(c => c.Order == 0 && c.Name == "name.firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && c.HelpTextWarning == "Mandatory First Name" && (string)c.Value == "Bob");
|
||||
schema.Should().Contain(c => c.Order == 1 && c.Name == "name.lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && c.HelpTextWarning == "Mandatory Last Name" && (string)c.Value == "Poop");
|
||||
schema.Should().Contain(c => c.Order == 2 && c.Name == "quote" && c.Label == "Quote" && c.HelpText == "Your Favorite Quote");
|
||||
}
|
||||
}
|
||||
|
||||
public class TestModel
|
||||
{
|
||||
[FieldDefinition(0, Label = "First Name", HelpText = "Your First Name")]
|
||||
[FieldDefinition(0, Label = "First Name", HelpText = "Your First Name", HelpTextWarning = "Mandatory First Name")]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Last Name", HelpText = "Your Last Name")]
|
||||
[FieldDefinition(1, Label = "Last Name", HelpText = "Your Last Name", HelpTextWarning = "Mandatory Last Name")]
|
||||
public string LastName { get; set; }
|
||||
|
||||
public string Other { get; set; }
|
||||
}
|
||||
|
||||
public class NestedTestModel
|
||||
{
|
||||
[FieldDefinition(0)]
|
||||
public TestModel Name { get; set; } = new TestModel();
|
||||
|
||||
[FieldDefinition(1, Label = "Quote", HelpText = "Your Favorite Quote")]
|
||||
public string Quote { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
// Indexer Urls
|
||||
[TestCase(@"https://iptorrents.com/torrents/rss?u=mySecret;tp=mySecret;l5;download")]
|
||||
[TestCase(@"http://rss.torrentleech.org/mySecret")]
|
||||
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/filename.torrent")]
|
||||
[TestCase(@"https://rss24h.torrentleech.org/mySecret")]
|
||||
[TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
|
||||
[TestCase(@"https://www.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")]
|
||||
[TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")]
|
||||
[TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=sonarr&api=mySecret&eng=1")]
|
||||
[TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")]
|
||||
@@ -44,6 +46,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
// Deluge
|
||||
[TestCase(@",{""download_location"": ""C:\Users\\mySecret mySecret\\Downloads""}")]
|
||||
[TestCase(@",{""download_location"": ""/home/mySecret/Downloads""}")]
|
||||
[TestCase(@",{""download_location"": ""/Users/mySecret/Downloads""}")]
|
||||
[TestCase(@"auth.login(""mySecret"")")]
|
||||
|
||||
// Download Station
|
||||
@@ -59,8 +62,11 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
|
||||
// Internal
|
||||
[TestCase(@"OutputPath=/home/mySecret/Downloads")]
|
||||
[TestCase(@"OutputPath=/Users/mySecret/Downloads")]
|
||||
[TestCase("Hardlinking episode file: /home/mySecret/Downloads to /media/abc.mkv")]
|
||||
[TestCase("Hardlinking episode file: /Users/mySecret/Downloads to /media/abc.mkv")]
|
||||
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||
[TestCase("Hardlink '/Users/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||
|
||||
@@ -158,6 +158,25 @@ namespace NzbDrone.Common.Test
|
||||
path.GetParentPath().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test\mydir", "Test")]
|
||||
[TestCase(@"C:\Test\", @"C:\")]
|
||||
[TestCase(@"C:\", null)]
|
||||
[TestCase(@"\\server\share", null)]
|
||||
[TestCase(@"\\server\share\test", @"\\server\share")]
|
||||
public void path_should_return_parent_name_windows(string path, string parentPath)
|
||||
{
|
||||
WindowsOnly();
|
||||
path.GetParentName().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[TestCase(@"/", null)]
|
||||
[TestCase(@"/test", "/")]
|
||||
public void path_should_return_parent_name_mono(string path, string parentPath)
|
||||
{
|
||||
PosixOnly();
|
||||
path.GetParentName().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void path_should_return_parent_for_oversized_path()
|
||||
{
|
||||
|
||||
@@ -87,9 +87,7 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
public static string GetParentPath(this string childPath)
|
||||
{
|
||||
var cleanPath = OsInfo.IsWindows
|
||||
? PARENT_PATH_END_SLASH_REGEX.Replace(childPath, "")
|
||||
: childPath.TrimEnd(Path.DirectorySeparatorChar);
|
||||
var cleanPath = childPath.GetCleanPath();
|
||||
|
||||
if (cleanPath.IsNullOrWhiteSpace())
|
||||
{
|
||||
@@ -99,6 +97,18 @@ namespace NzbDrone.Common.Extensions
|
||||
return Directory.GetParent(cleanPath)?.FullName;
|
||||
}
|
||||
|
||||
public static string GetParentName(this string childPath)
|
||||
{
|
||||
var cleanPath = childPath.GetCleanPath();
|
||||
|
||||
if (cleanPath.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Directory.GetParent(cleanPath)?.Name;
|
||||
}
|
||||
|
||||
public static string GetCleanPath(this string path)
|
||||
{
|
||||
var cleanPath = OsInfo.IsWindows
|
||||
|
||||
@@ -60,8 +60,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
if (request.AllowAutoRedirect && response.HasHttpRedirect)
|
||||
{
|
||||
var autoRedirectChain = new List<string>();
|
||||
autoRedirectChain.Add(request.Url.ToString());
|
||||
var autoRedirectChain = new List<string> { request.Url.ToString() };
|
||||
|
||||
do
|
||||
{
|
||||
@@ -75,6 +74,14 @@ namespace NzbDrone.Common.Http
|
||||
throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
|
||||
}
|
||||
|
||||
// 302 or 303 should default to GET on redirect even if POST on original
|
||||
if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
|
||||
{
|
||||
request.Method = HttpMethod.Get;
|
||||
request.ContentData = null;
|
||||
request.ContentSummary = null;
|
||||
}
|
||||
|
||||
response = ExecuteRequest(request, cookieContainer);
|
||||
}
|
||||
while (response.HasHttpRedirect);
|
||||
@@ -105,6 +112,16 @@ namespace NzbDrone.Common.Http
|
||||
return response;
|
||||
}
|
||||
|
||||
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
|
||||
{
|
||||
return statusCode switch
|
||||
{
|
||||
HttpStatusCode.Moved or HttpStatusCode.Found or HttpStatusCode.MultipleChoices => requestMethod == HttpMethod.Post,
|
||||
HttpStatusCode.SeeOther => requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer)
|
||||
{
|
||||
foreach (var interceptor in _requestInterceptors)
|
||||
|
||||
@@ -50,6 +50,8 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public bool HasHttpError => (int)StatusCode >= 400;
|
||||
|
||||
public bool HasHttpServerError => (int)StatusCode >= 500;
|
||||
|
||||
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
|
||||
StatusCode == HttpStatusCode.MovedPermanently ||
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
|
||||
@@ -7,55 +7,55 @@ namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class CleanseLogMessage
|
||||
{
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
private static readonly Regex[] CleansingRules =
|
||||
{
|
||||
// Url
|
||||
new (@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"rss(24h)?\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
|
||||
// Path
|
||||
new Regex(@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Path
|
||||
new (@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"/(home|Users)/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// NzbGet
|
||||
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// NzbGet
|
||||
new (@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Sabnzbd
|
||||
new Regex(@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Sabnzbd
|
||||
new (@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// uTorrent
|
||||
new Regex(@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// uTorrent
|
||||
new (@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Deluge
|
||||
new Regex(@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Deluge
|
||||
new (@"auth.login\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// BroadcastheNet
|
||||
new Regex(@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// BroadcastheNet
|
||||
new (@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"getTorrents\(""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Plex
|
||||
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Plex
|
||||
new (@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Notifiarr
|
||||
new Regex(@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
// Notifiarr
|
||||
new (@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Discord
|
||||
new Regex(@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
// Discord
|
||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
public static string Cleanse(string message)
|
||||
{
|
||||
@@ -67,15 +67,15 @@ namespace NzbDrone.Common.Instrumentation
|
||||
foreach (var regex in CleansingRules)
|
||||
{
|
||||
message = regex.Replace(message, m =>
|
||||
{
|
||||
var value = m.Value;
|
||||
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
|
||||
{
|
||||
var value = m.Value;
|
||||
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
|
||||
{
|
||||
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
|
||||
}
|
||||
value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)");
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
message = CleanseRemoteIPRegex.Replace(message, CleanseRemoteIP);
|
||||
@@ -86,7 +86,6 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static string CleanseRemoteIP(Match match)
|
||||
{
|
||||
var group = match.Groups[1];
|
||||
var valueAll = match.Value;
|
||||
var valueIP = group.Value;
|
||||
|
||||
if (IPAddress.TryParse(valueIP, out var address) && !address.IsLocalAddress())
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
else
|
||||
{
|
||||
dsn = RuntimeInfo.IsProduction
|
||||
? "https://26668106d708406b9ddf5a2bda34fcbb@sentry.servarr.com/9"
|
||||
? "https://40f1288c1b4d495cbafdb5c89f7f01be@sentry.servarr.com/9"
|
||||
: "https://998b4673d4c849ccb5277b5966ed5bc2@sentry.servarr.com/10";
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.Dsn = dsn;
|
||||
o.AttachStacktrace = true;
|
||||
o.MaxBreadcrumbs = 200;
|
||||
o.Release = BuildInfo.Release;
|
||||
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
o.Environment = BuildInfo.Branch;
|
||||
|
||||
@@ -121,6 +121,11 @@ namespace NzbDrone.Common.Serializer
|
||||
return JsonConvert.SerializeObject(obj, SerializerSettings);
|
||||
}
|
||||
|
||||
public static string ToJson(this object obj, Formatting formatting)
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj, formatting, SerializerSettings);
|
||||
}
|
||||
|
||||
public static void Serialize<TModel>(TModel model, TextWriter outputStream)
|
||||
{
|
||||
var jsonTextWriter = new JsonTextWriter(outputStream);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class remove_invalid_roksbox_metadata_imagesFixture : MigrationTest<remove_invalid_roksbox_metadata_images>
|
||||
{
|
||||
[Test]
|
||||
public void should_remove_incorrect_roksbox_metadata_images()
|
||||
{
|
||||
var db = WithDapperMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("MetadataFiles").Row(new
|
||||
{
|
||||
MovieId = 1,
|
||||
Consumer = "RoksboxMetadata",
|
||||
Type = 5,
|
||||
RelativePath = @"metadata\Movie Title (2023).jpg",
|
||||
LastUpdated = "2023-01-21 00:00:00.000",
|
||||
Added = "2023-01-21 00:00:00.000",
|
||||
MovieFileId = 1,
|
||||
Extension = ".jpg"
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("MetadataFiles").Row(new
|
||||
{
|
||||
MovieId = 1,
|
||||
Consumer = "RoksboxMetadata",
|
||||
Type = 5,
|
||||
RelativePath = @"Movie Title (2023).jpg",
|
||||
LastUpdated = "2023-01-21 00:00:00.000",
|
||||
MovieFileId = 1,
|
||||
Added = "2023-01-21 00:00:00.000",
|
||||
Extension = ".jpg"
|
||||
});
|
||||
});
|
||||
|
||||
var metadataFiles = db.Query<MetadataFile223>("SELECT * FROM \"MetadataFiles\"");
|
||||
|
||||
metadataFiles.Should().HaveCount(1);
|
||||
metadataFiles.First().RelativePath.Should().NotContain("metadata");
|
||||
}
|
||||
}
|
||||
|
||||
public class MetadataFile223
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MovieId { get; set; }
|
||||
public int? MovieFileId { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public string Extension { get; set; }
|
||||
public string Hash { get; set; }
|
||||
public string Consumer { get; set; }
|
||||
public int Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
[TestCase("checkingDL")]
|
||||
[TestCase("checkingUP")]
|
||||
[TestCase("metaDL")]
|
||||
[TestCase("checkingResumeData")]
|
||||
public void queued_item_should_have_required_properties(string state)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
_blockedLists = new List<ImportListStatus>();
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.Enabled())
|
||||
.Setup(v => v.Enabled(It.IsAny<bool>()))
|
||||
.Returns(_importLists);
|
||||
|
||||
Mocker.GetMock<IImportListStatusService>()
|
||||
@@ -183,7 +183,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
listResult.Movies.Count.Should().Be(10);
|
||||
listResult.Movies.Count.Should().Be(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
};
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.Enabled())
|
||||
.Setup(v => v.Enabled(It.IsAny<bool>()))
|
||||
.Returns(_importLists);
|
||||
|
||||
Mocker.GetMock<IImportExclusionsService>()
|
||||
@@ -156,6 +156,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
.Setup(v => v.GetAllMovies())
|
||||
.Returns(_existingMovies);
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Setup(v => v.GetAllListMovies())
|
||||
.Returns(_list1Movies);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
@@ -179,6 +183,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
.Setup(v => v.GetAllMovies())
|
||||
.Returns(_existingMovies);
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Setup(v => v.GetAllListMovies())
|
||||
.Returns(_list1Movies);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
@@ -204,6 +212,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
.Setup(v => v.GetAllMovies())
|
||||
.Returns(_existingMovies);
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Setup(v => v.GetAllListMovies())
|
||||
.Returns(_list1Movies);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
@@ -224,6 +236,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
.Setup(v => v.GetAllMovies())
|
||||
.Returns(_existingMovies);
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Setup(v => v.GetAllListMovies())
|
||||
.Returns(_list1Movies);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
@@ -241,6 +257,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
.Setup(v => v.GetAllMovies())
|
||||
.Returns(_existingMovies);
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Setup(v => v.GetAllListMovies())
|
||||
.Returns(_list1Movies);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
@@ -267,6 +287,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
.Setup(v => v.GetAllMovies())
|
||||
.Returns(_existingMovies);
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Setup(v => v.GetAllListMovies())
|
||||
.Returns(_list1Movies);
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
@@ -347,7 +371,7 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_add_duplicate_movies_from_seperate_lists()
|
||||
public void should_not_add_duplicate_movies_from_separate_lists()
|
||||
{
|
||||
_list2Movies.ForEach(m => m.ListId = 2);
|
||||
_importListFetch.Movies.ForEach(m => m.ListId = 1);
|
||||
@@ -384,7 +408,7 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddMovieService>()
|
||||
.Verify(v => v.AddMovies(It.Is<List<Movie>>(s => s.Count == 7 && !s.Any(m => m.TmdbId == _existingMovies[0].TmdbId)), true), Times.Once());
|
||||
.Verify(v => v.AddMovies(It.Is<List<Movie>>(s => s.Count == 7 && s.All(m => m.TmdbId != _existingMovies[0].TmdbId)), true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -406,7 +430,7 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddMovieService>()
|
||||
.Verify(v => v.AddMovies(It.Is<List<Movie>>(s => s.Count == 7 && !s.Any(m => m.TmdbId == _existingMovies[0].TmdbId)), true), Times.Once());
|
||||
.Verify(v => v.AddMovies(It.Is<List<Movie>>(s => s.Count == 7 && s.All(m => m.TmdbId != _existingMovies[0].TmdbId)), true), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("The German 2021 Bluray AVC")]
|
||||
[TestCase("German.Only.Movie.2021.French.1080p.BluRay.AVC-UNTAVC")]
|
||||
[TestCase("Movie.Title.2008.US.Directors.Cut.UHD.BD66.Blu-ray")]
|
||||
[TestCase("Movie.2009.Blu.ray.AVC.DTS.HD.MA.5.1")]
|
||||
public void should_parse_brdisk_1080p_quality(string title)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.BLURAY, false, Resolution.R1080p, Modifier.BRDISK);
|
||||
|
||||
@@ -109,6 +109,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Should.Not.Use.Dots.2022.1080p.BluRay.x265.10bit.Tigole", "Tigole")]
|
||||
[TestCase("Movie.Title.2005.2160p.UHD.BluRay.TrueHD 7.1.Atmos.x265 - HQMUX", "HQMUX")]
|
||||
[TestCase("Movie.Name.2022.1080p.BluRay.x264-VARYG (Blue Lock, Multi-Subs)", "VARYG")]
|
||||
[TestCase("Movie Title (2023) (1080p BluRay x265 SDR AAC 2.0 English Vyndros)", "Vyndros")]
|
||||
public void should_parse_exception_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
|
||||
43
src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs
Normal file
43
src/NzbDrone.Core.Test/ParserTests/UrlFixture.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class UrlFixture : CoreTest
|
||||
{
|
||||
[TestCase("[www.test.com] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("test.net - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[www.test-hyphen.com] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("www.test123.org - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[test.co.uk] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("www.test-hyphen.net.au - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[www.test123.co.nz] - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("test-hyphen123.org.au - Movie.Title.2023.720p.HDTV.X264-DIMENSION", "Movie Title")]
|
||||
[TestCase("[www.test123.de] - Mad Movie Title 2023 [Bluray720p]", "Mad Movie Title")]
|
||||
[TestCase("www.test-hyphen.de - Mad Movie Title 2023 [Bluray1080p]", "Mad Movie Title")]
|
||||
[TestCase("www.test123.co.za - The Movie Title Bros. (2023)", "The Movie Title Bros.")]
|
||||
[TestCase("[www.test-hyphen.ca] - Movie Title (2023)", "Movie Title")]
|
||||
[TestCase("test123.ca - Movie Time 2023 720p HDTV x264 CRON", "Movie Time")]
|
||||
[TestCase("[www.test-hyphen123.co.za] - Movie Title 2023", "Movie Title")]
|
||||
public void should_not_parse_url_in_name(string postTitle, string title)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle).MovieTitle.CleanMovieTitle();
|
||||
result.Should().Be(title.CleanMovieTitle());
|
||||
}
|
||||
|
||||
[TestCase("Movie.2023.English.HDTV.XviD-LOL[www.abb.com]", "LOL")]
|
||||
[TestCase("Movie Title 2023 English HDTV XviD LOL[www.academy.org]", null)]
|
||||
[TestCase("Movie Title Now 2023 DVDRip XviD RUNNER[www.aetna.net]", null)]
|
||||
[TestCase("Movie.Title.2023.DVDRip.XviD-RUNNER[www.alfaromeo.io]", "RUNNER")]
|
||||
[TestCase("Movie.Title.2023.English.HDTV.XviD-LOL[www.abbott.gov]", "LOL")]
|
||||
[TestCase("Movie Title 2023 English HDTV XviD LOL[www.actor.org]", null)]
|
||||
[TestCase("Movie Title Future 2023 DVDRip XviD RUNNER[www.allstate.net]", null)]
|
||||
public void should_not_parse_url_in_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Label { get; set; }
|
||||
public string Unit { get; set; }
|
||||
public string HelpText { get; set; }
|
||||
public string HelpTextWarning { get; set; }
|
||||
public string HelpLink { get; set; }
|
||||
public FieldType Type { get; set; }
|
||||
public bool Advanced { get; set; }
|
||||
|
||||
@@ -116,13 +116,6 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("AvailabilityDelay", value); }
|
||||
}
|
||||
|
||||
public int ImportListSyncInterval
|
||||
{
|
||||
get { return GetValueInt("ImportListSyncInterval", 24); }
|
||||
|
||||
set { SetValue("ImportListSyncInterval", value); }
|
||||
}
|
||||
|
||||
public string ListSyncLevel
|
||||
{
|
||||
get { return GetValue("ListSyncLevel", "disabled"); }
|
||||
|
||||
@@ -60,7 +60,6 @@ namespace NzbDrone.Core.Configuration
|
||||
bool AllowHardcodedSubs { get; set; }
|
||||
string WhitelistedHardcodedSubs { get; set; }
|
||||
|
||||
int ImportListSyncInterval { get; set; }
|
||||
string ListSyncLevel { get; set; }
|
||||
string ImportExclusions { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(223)]
|
||||
public class remove_invalid_roksbox_metadata_images : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
IfDatabase("sqlite").Execute.Sql("DELETE FROM \"MetadataFiles\" WHERE \"Consumer\" = 'RoksboxMetadata' AND \"Type\" = 5 AND (\"RelativePath\" LIKE 'metadata/%' OR \"RelativePath\" LIKE 'metadata\\%')");
|
||||
IfDatabase("postgresql").Execute.Sql("DELETE FROM \"MetadataFiles\" WHERE \"Consumer\" = 'RoksboxMetadata' AND \"Type\" = 5 AND (\"RelativePath\" LIKE 'metadata/%' OR \"RelativePath\" LIKE 'metadata\\\\%')");
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/NzbDrone.Core/Datastore/Migration/224_list_sync_time.cs
Normal file
18
src/NzbDrone.Core/Datastore/Migration/224_list_sync_time.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(224)]
|
||||
public class list_sync_time : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Delete.Column("LastSyncListInfo").FromTable("ImportListStatus");
|
||||
|
||||
Alter.Table("ImportListStatus").AddColumn("LastInfoSync").AsDateTimeOffset().Nullable();
|
||||
|
||||
Delete.FromTable("Config").Row(new { Key = "importlistsyncinterval" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(225)]
|
||||
public class add_tags_to_collections : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Collections").AddColumn("Tags").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ namespace NzbDrone.Core.Datastore
|
||||
Mapper.Entity<ImportListDefinition>("ImportLists").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName)
|
||||
.Ignore(i => i.ListType)
|
||||
.Ignore(i => i.MinRefreshInterval)
|
||||
.Ignore(i => i.Enable);
|
||||
|
||||
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
|
||||
|
||||
@@ -251,6 +251,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
case "queuedDL": // queuing is enabled and torrent is queued for download
|
||||
case "checkingDL": // same as checkingUP, but torrent has NOT finished downloading
|
||||
case "checkingUP": // torrent has finished downloading and is being checked. Set when `recheck torrent on completion` is enabled. In the event the check fails we shouldn't treat it as completed.
|
||||
case "checkingResumeData": // torrent is checking resume data on load
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
break;
|
||||
|
||||
|
||||
@@ -84,7 +84,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
|
||||
|
||||
if (extension == ".jpg")
|
||||
{
|
||||
if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
!path.GetParentName().Equals("metadata", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
metadata.Type = MetadataType.MovieImage;
|
||||
return metadata;
|
||||
|
||||
@@ -48,13 +48,11 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
try
|
||||
{
|
||||
var status = client.GetStatus();
|
||||
var folders = status.OutputRootFolders;
|
||||
var folders = status.OutputRootFolders.Where(folder => rootFolders.Any(r => r.Path.PathEquals(folder.FullPath)));
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
if (rootFolders.Any(r => r.Path.PathEquals(folder.FullPath)))
|
||||
{
|
||||
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, string.Format(_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot"), client.Definition.Name, folder.FullPath), "#downloads-in-root-folder");
|
||||
}
|
||||
}
|
||||
catch (DownloadClientException ex)
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var clients = _downloadClientProvider.GetDownloadClients();
|
||||
var clients = _downloadClientProvider.GetDownloadClients(true);
|
||||
|
||||
foreach (var client in clients)
|
||||
{
|
||||
|
||||
@@ -23,12 +23,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var jackettAllProviders = _providerFactory.All().Where(
|
||||
i => i.ConfigContract.Equals("TorznabSettings") &&
|
||||
((i.Settings as TorznabSettings).BaseUrl.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
(i.Settings as TorznabSettings).BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
(i.Settings as TorznabSettings).ApiPath.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
(i.Settings as TorznabSettings).ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase)));
|
||||
var jackettAllProviders = _providerFactory.All()
|
||||
.Where(
|
||||
i => i.ConfigContract.Equals("TorznabSettings") &&
|
||||
(((TorznabSettings)i.Settings).BaseUrl.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
((TorznabSettings)i.Settings).BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
((TorznabSettings)i.Settings).ApiPath.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
((TorznabSettings)i.Settings).ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase)))
|
||||
.ToArray();
|
||||
|
||||
if (jackettAllProviders.Empty())
|
||||
{
|
||||
|
||||
@@ -28,13 +28,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var enabledProviders = _providerFactory.GetAvailableProviders();
|
||||
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.Before(
|
||||
DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.Before(DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
|
||||
if (backOffProviders.Empty())
|
||||
{
|
||||
|
||||
@@ -26,13 +26,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var enabledProviders = _providerFactory.GetAvailableProviders();
|
||||
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.After(
|
||||
DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
i => i.Definition.Id,
|
||||
s => s.ProviderId,
|
||||
(i, s) => new { Provider = i, Status = s })
|
||||
.Where(p => p.Status.InitialFailure.HasValue &&
|
||||
p.Status.InitialFailure.Value.After(DateTime.UtcNow.AddHours(-6)))
|
||||
.ToList();
|
||||
|
||||
if (backOffProviders.Empty())
|
||||
{
|
||||
|
||||
@@ -21,10 +21,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
// Not best for optimization but due to possible symlinks and junctions, we get mounts based on series path so internals can handle mount resolution.
|
||||
var mounts = _movieService.AllMoviePaths()
|
||||
.Select(p => _diskProvider.GetMount(p.Value))
|
||||
.Where(m => m != null && m.MountOptions != null && m.MountOptions.IsReadOnly)
|
||||
.DistinctBy(m => m.RootDirectory)
|
||||
.ToList();
|
||||
.Select(p => _diskProvider.GetMount(p.Value))
|
||||
.Where(m => m is { MountOptions.IsReadOnly: true })
|
||||
.DistinctBy(m => m.RootDirectory)
|
||||
.ToList();
|
||||
|
||||
if (mounts.Any())
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user