mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 381ce61aef | |||
| 9f705e4161 | |||
| 063dba22a8 | |||
| 6d552f2a60 | |||
| 4d4d63921b | |||
| 6584d95331 | |||
| 86034beccd | |||
| 4aa56e3f91 |
@@ -121,7 +121,7 @@ jobs:
|
|||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
|
||||||
- name: Stylelint
|
- name: Stylelint
|
||||||
run: yarn stylelint
|
run: yarn stylelint -f github
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build --env production
|
run: yarn build --env production
|
||||||
@@ -225,3 +225,25 @@ jobs:
|
|||||||
branch: ${{ github.ref_name }}
|
branch: ${{ github.ref_name }}
|
||||||
major_version: ${{ needs.backend.outputs.major_version }}
|
major_version: ${{ needs.backend.outputs.major_version }}
|
||||||
version: ${{ needs.backend.outputs.version }}
|
version: ${{ needs.backend.outputs.version }}
|
||||||
|
|
||||||
|
notify:
|
||||||
|
name: Discord Notification
|
||||||
|
needs: [backend, unit_test, unit_test_postgres, integration_test]
|
||||||
|
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
|
||||||
|
env:
|
||||||
|
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify
|
||||||
|
uses: tsickert/discord-webhook@v5.3.0
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
username: 'GitHub Actions'
|
||||||
|
avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
|
||||||
|
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
|
||||||
|
embed-url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||||
|
embed-description: |
|
||||||
|
**Branch** ${{ github.ref }}
|
||||||
|
**Build** ${{ needs.backend.outputs.version }}
|
||||||
|
embed-color: ${{ env.STATUS == 'success' && '3066993' || '15158332' }}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface CommandBody {
|
|||||||
trigger: string;
|
trigger: string;
|
||||||
suppressMessages: boolean;
|
suppressMessages: boolean;
|
||||||
seriesId?: number;
|
seriesId?: number;
|
||||||
|
seriesIds?: number[];
|
||||||
|
seasonNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Command extends ModelBase {
|
interface Command extends ModelBase {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import monitorOptions from 'Utilities/Series/monitorOptions';
|
import monitorOptions from 'Utilities/Series/monitorOptions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MonitorEpisodesSelectInput(props) {
|
function MonitorEpisodesSelectInput(props) {
|
||||||
const {
|
const {
|
||||||
@@ -19,7 +19,7 @@ function MonitorEpisodesSelectInput(props) {
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +29,12 @@ function MonitorEpisodesSelectInput(props) {
|
|||||||
get value() {
|
get value() {
|
||||||
return `(${translate('Mixed')})`;
|
return `(${translate('Mixed')})`;
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<EnhancedSelectInput
|
||||||
values={values}
|
values={values}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
||||||
import SelectInput from './SelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MonitorNewItemsSelectInput(props) {
|
function MonitorNewItemsSelectInput(props) {
|
||||||
const {
|
const {
|
||||||
@@ -16,7 +16,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: 'No Change',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ function MonitorNewItemsSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<EnhancedSelectInput
|
||||||
values={values}
|
values={values}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function createMapStateToProps() {
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: includeNoChangeDisabled
|
isDisabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ function createMapStateToProps() {
|
|||||||
get value() {
|
get value() {
|
||||||
return `(${translate('Mixed')})`;
|
return `(${translate('Mixed')})`;
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface ISeriesTypeOption {
|
|||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
format?: string;
|
format?: string;
|
||||||
disabled?: boolean;
|
isDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesTypeOptions: ISeriesTypeOption[] = [
|
const seriesTypeOptions: ISeriesTypeOption[] = [
|
||||||
@@ -55,7 +55,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: includeNoChangeDisabled,
|
isDisabled: includeNoChangeDisabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: `(${translate('Mixed')})`,
|
value: `(${translate('Mixed')})`,
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const monitoredOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'monitored',
|
key: 'monitored',
|
||||||
@@ -58,7 +58,7 @@ const seasonFolderOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'yes',
|
key: 'yes',
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ const enableOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ const autoAddOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ const enableOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
.token {
|
.token {
|
||||||
flex: 0 0 50%;
|
flex: 0 0 50%;
|
||||||
padding: 6px 6px;
|
padding: 6px;
|
||||||
background-color: var(--popoverTitleBackgroundColor);
|
background-color: var(--popoverTitleBackgroundColor);
|
||||||
font-family: $monoSpaceFontFamily;
|
font-family: $monoSpaceFontFamily;
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex: 0 0 50%;
|
flex: 0 0 50%;
|
||||||
padding: 6px 6px;
|
padding: 6px;
|
||||||
background-color: var(--popoverBodyBackgroundColor);
|
background-color: var(--popoverBodyBackgroundColor);
|
||||||
|
|
||||||
.footNote {
|
.footNote {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createMultiSeriesSelector(seriesIds: number[]) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.series.itemMap,
|
||||||
|
(state: AppState) => state.series.items,
|
||||||
|
(itemMap, allSeries) => {
|
||||||
|
return seriesIds.map((seriesId) => allSeries[itemMap[seriesId]]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createMultiSeriesSelector;
|
||||||
@@ -10,15 +10,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandName {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userAgent {
|
|
||||||
color: #b0b0b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queued,
|
.queued,
|
||||||
.started,
|
.started,
|
||||||
.ended {
|
.ended {
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'commandName': string;
|
|
||||||
'duration': string;
|
'duration': string;
|
||||||
'ended': string;
|
'ended': string;
|
||||||
'queued': string;
|
'queued': string;
|
||||||
'started': string;
|
'started': string;
|
||||||
'trigger': string;
|
'trigger': string;
|
||||||
'triggerContent': string;
|
'triggerContent': string;
|
||||||
'userAgent': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,279 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import formatDate from 'Utilities/Date/formatDate';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './QueuedTaskRow.css';
|
|
||||||
|
|
||||||
function getStatusIconProps(status, message) {
|
|
||||||
const title = titleCase(status);
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case 'queued':
|
|
||||||
return {
|
|
||||||
name: icons.PENDING,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'started':
|
|
||||||
return {
|
|
||||||
name: icons.REFRESH,
|
|
||||||
isSpinning: true,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'completed':
|
|
||||||
return {
|
|
||||||
name: icons.CHECK,
|
|
||||||
kind: kinds.SUCCESS,
|
|
||||||
title: message === 'Completed' ? title : `${title}: ${message}`
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'failed':
|
|
||||||
return {
|
|
||||||
name: icons.FATAL,
|
|
||||||
kind: kinds.DANGER,
|
|
||||||
title: `${title}: ${message}`
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
name: icons.UNKNOWN,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormattedDates(props) {
|
|
||||||
const {
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (showRelativeDates) {
|
|
||||||
return {
|
|
||||||
queuedAt: moment(queued).fromNow(),
|
|
||||||
startedAt: started ? moment(started).fromNow() : '-',
|
|
||||||
endedAt: ended ? moment(ended).fromNow() : '-'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
queuedAt: formatDate(queued, shortDateFormat),
|
|
||||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
|
||||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueuedTaskRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
...getFormattedDates(props),
|
|
||||||
isCancelConfirmModalOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this._updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setUpdateTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
queued !== prevProps.queued ||
|
|
||||||
started !== prevProps.started ||
|
|
||||||
ended !== prevProps.ended
|
|
||||||
) {
|
|
||||||
this.setState(getFormattedDates(this.props));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._updateTimeoutId) {
|
|
||||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setUpdateTimer() {
|
|
||||||
this._updateTimeoutId = setTimeout(() => {
|
|
||||||
this.setState(getFormattedDates(this.props));
|
|
||||||
this.setUpdateTimer();
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onCancelPress = () => {
|
|
||||||
this.setState({
|
|
||||||
isCancelConfirmModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onAbortCancel = () => {
|
|
||||||
this.setState({
|
|
||||||
isCancelConfirmModalOpen: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
trigger,
|
|
||||||
commandName,
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended,
|
|
||||||
status,
|
|
||||||
duration,
|
|
||||||
message,
|
|
||||||
clientUserAgent,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onCancelPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
queuedAt,
|
|
||||||
startedAt,
|
|
||||||
endedAt,
|
|
||||||
isCancelConfirmModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
let triggerIcon = icons.QUICK;
|
|
||||||
|
|
||||||
if (trigger === 'manual') {
|
|
||||||
triggerIcon = icons.INTERACTIVE;
|
|
||||||
} else if (trigger === 'scheduled') {
|
|
||||||
triggerIcon = icons.SCHEDULED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableRowCell className={styles.trigger}>
|
|
||||||
<span className={styles.triggerContent}>
|
|
||||||
<Icon
|
|
||||||
name={triggerIcon}
|
|
||||||
title={titleCase(trigger)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
{...getStatusIconProps(status, message)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>
|
|
||||||
<span className={styles.commandName}>
|
|
||||||
{commandName}
|
|
||||||
</span>
|
|
||||||
{
|
|
||||||
clientUserAgent ?
|
|
||||||
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
|
|
||||||
{translate('From')}: {clientUserAgent}
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.queued}
|
|
||||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{queuedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.started}
|
|
||||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{startedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.ended}
|
|
||||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{endedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.duration}>
|
|
||||||
{formatTimeSpan(duration)}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
status === 'queued' &&
|
|
||||||
<IconButton
|
|
||||||
title={translate('RemovedFromTaskQueue')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
onPress={this.onCancelPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isCancelConfirmModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('Cancel')}
|
|
||||||
message={translate('CancelPendingTask')}
|
|
||||||
confirmLabel={translate('YesCancel')}
|
|
||||||
cancelLabel={translate('NoLeaveIt')}
|
|
||||||
onConfirm={onCancelPress}
|
|
||||||
onCancel={this.onAbortCancel}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTaskRow.propTypes = {
|
|
||||||
trigger: PropTypes.string.isRequired,
|
|
||||||
commandName: PropTypes.string.isRequired,
|
|
||||||
queued: PropTypes.string.isRequired,
|
|
||||||
started: PropTypes.string,
|
|
||||||
ended: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
duration: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
clientUserAgent: PropTypes.string,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onCancelPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueuedTaskRow;
|
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { CommandBody } from 'Commands/Command';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import formatDate from 'Utilities/Date/formatDate';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
|
||||||
|
import styles from './QueuedTaskRow.css';
|
||||||
|
|
||||||
|
function getStatusIconProps(status: string, message: string | undefined) {
|
||||||
|
const title = titleCase(status);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'queued':
|
||||||
|
return {
|
||||||
|
name: icons.PENDING,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'started':
|
||||||
|
return {
|
||||||
|
name: icons.REFRESH,
|
||||||
|
isSpinning: true,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'completed':
|
||||||
|
return {
|
||||||
|
name: icons.CHECK,
|
||||||
|
kind: kinds.SUCCESS,
|
||||||
|
title: message === 'Completed' ? title : `${title}: ${message}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'failed':
|
||||||
|
return {
|
||||||
|
name: icons.FATAL,
|
||||||
|
kind: kinds.DANGER,
|
||||||
|
title: `${title}: ${message}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
name: icons.UNKNOWN,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedDates(
|
||||||
|
queued: string,
|
||||||
|
started: string | undefined,
|
||||||
|
ended: string | undefined,
|
||||||
|
showRelativeDates: boolean,
|
||||||
|
shortDateFormat: string
|
||||||
|
) {
|
||||||
|
if (showRelativeDates) {
|
||||||
|
return {
|
||||||
|
queuedAt: moment(queued).fromNow(),
|
||||||
|
startedAt: started ? moment(started).fromNow() : '-',
|
||||||
|
endedAt: ended ? moment(ended).fromNow() : '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queuedAt: formatDate(queued, shortDateFormat),
|
||||||
|
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||||
|
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueuedTimes {
|
||||||
|
queuedAt: string;
|
||||||
|
startedAt: string;
|
||||||
|
endedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueuedTaskRowProps {
|
||||||
|
id: number;
|
||||||
|
trigger: string;
|
||||||
|
commandName: string;
|
||||||
|
queued: string;
|
||||||
|
started?: string;
|
||||||
|
ended?: string;
|
||||||
|
status: string;
|
||||||
|
duration?: string;
|
||||||
|
message?: string;
|
||||||
|
body: CommandBody;
|
||||||
|
clientUserAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
trigger,
|
||||||
|
commandName,
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
status,
|
||||||
|
duration,
|
||||||
|
message,
|
||||||
|
body,
|
||||||
|
clientUserAgent,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
|
||||||
|
useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [times, setTimes] = useState<QueuedTimes>(
|
||||||
|
getFormattedDates(
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isCancelConfirmModalOpen,
|
||||||
|
openCancelConfirmModal,
|
||||||
|
closeCancelConfirmModal,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const handleCancelPress = useCallback(() => {
|
||||||
|
dispatch(cancelCommand({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTimeTimeoutId.current = setTimeout(() => {
|
||||||
|
setTimes(
|
||||||
|
getFormattedDates(
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (updateTimeTimeoutId.current) {
|
||||||
|
clearTimeout(updateTimeTimeoutId.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
|
||||||
|
|
||||||
|
const { queuedAt, startedAt, endedAt } = times;
|
||||||
|
|
||||||
|
let triggerIcon = icons.QUICK;
|
||||||
|
|
||||||
|
if (trigger === 'manual') {
|
||||||
|
triggerIcon = icons.INTERACTIVE;
|
||||||
|
} else if (trigger === 'scheduled') {
|
||||||
|
triggerIcon = icons.SCHEDULED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell className={styles.trigger}>
|
||||||
|
<span className={styles.triggerContent}>
|
||||||
|
<Icon name={triggerIcon} title={titleCase(trigger)} />
|
||||||
|
|
||||||
|
<Icon {...getStatusIconProps(status, message)} />
|
||||||
|
</span>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<QueuedTaskRowNameCell
|
||||||
|
commandName={commandName}
|
||||||
|
body={body}
|
||||||
|
clientUserAgent={clientUserAgent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.queued}
|
||||||
|
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{queuedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.started}
|
||||||
|
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{startedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.ended}
|
||||||
|
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{endedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.duration}>
|
||||||
|
{formatTimeSpan(duration)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
{status === 'queued' && (
|
||||||
|
<IconButton
|
||||||
|
title={translate('RemovedFromTaskQueue')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={openCancelConfirmModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isCancelConfirmModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('Cancel')}
|
||||||
|
message={translate('CancelPendingTask')}
|
||||||
|
confirmLabel={translate('YesCancel')}
|
||||||
|
cancelLabel={translate('NoLeaveIt')}
|
||||||
|
onConfirm={handleCancelPress}
|
||||||
|
onCancel={closeCancelConfirmModal}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import QueuedTaskRow from './QueuedTaskRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(uiSettings) => {
|
|
||||||
return {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onCancelPress() {
|
|
||||||
dispatch(cancelCommand({
|
|
||||||
id: props.id
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.commandName {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAgent {
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'commandName': string;
|
||||||
|
'userAgent': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { CommandBody } from 'Commands/Command';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './QueuedTaskRowNameCell.css';
|
||||||
|
|
||||||
|
export interface QueuedTaskRowNameCellProps {
|
||||||
|
commandName: string;
|
||||||
|
body: CommandBody;
|
||||||
|
clientUserAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueuedTaskRowNameCell(
|
||||||
|
props: QueuedTaskRowNameCellProps
|
||||||
|
) {
|
||||||
|
const { commandName, body, clientUserAgent } = props;
|
||||||
|
const seriesIds = [...(body.seriesIds ?? [])];
|
||||||
|
|
||||||
|
if (body.seriesId) {
|
||||||
|
seriesIds.push(body.seriesId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = useSelector(createMultiSeriesSelector(seriesIds));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell>
|
||||||
|
<span className={styles.commandName}>
|
||||||
|
{commandName}
|
||||||
|
{series.length ? (
|
||||||
|
<span> - {series.map((s) => s.title).join(', ')}</span>
|
||||||
|
) : null}
|
||||||
|
{body.seasonNumber ? (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
{translate('SeasonNumberToken', {
|
||||||
|
seasonNumber: body.seasonNumber,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{clientUserAgent ? (
|
||||||
|
<span
|
||||||
|
className={styles.userAgent}
|
||||||
|
title={translate('TaskUserAgentTooltip')}
|
||||||
|
>
|
||||||
|
{translate('From')}: {clientUserAgent}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'trigger',
|
|
||||||
label: '',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'commandName',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'queued',
|
|
||||||
label: () => translate('Queued'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'started',
|
|
||||||
label: () => translate('Started'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ended',
|
|
||||||
label: () => translate('Ended'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'duration',
|
|
||||||
label: () => translate('Duration'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function QueuedTasks(props) {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
items
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('Queue')}>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated &&
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QueuedTaskRowConnector
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
}
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTasks.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.array.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueuedTasks;
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueuedTaskRow from './QueuedTaskRow';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'trigger',
|
||||||
|
label: '',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'commandName',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'queued',
|
||||||
|
label: () => translate('Queued'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'started',
|
||||||
|
label: () => translate('Started'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ended',
|
||||||
|
label: () => translate('Ended'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duration',
|
||||||
|
label: () => translate('Duration'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function QueuedTasks() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, isPopulated, items } = useSelector(
|
||||||
|
(state: AppState) => state.commands
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchCommands());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('Queue')}>
|
||||||
|
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||||
|
|
||||||
|
{isPopulated && (
|
||||||
|
<Table columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <QueuedTaskRow key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
|
||||||
import QueuedTasks from './QueuedTasks';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.commands,
|
|
||||||
(commands) => {
|
|
||||||
return commands;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchCommands: fetchCommands
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueuedTasksConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QueuedTasks
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTasksConnector.propTypes = {
|
|
||||||
dispatchFetchCommands: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
import QueuedTasks from './Queued/QueuedTasks';
|
||||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||||
|
|
||||||
function Tasks() {
|
function Tasks() {
|
||||||
@@ -10,7 +10,7 @@ function Tasks() {
|
|||||||
<PageContent title={translate('Tasks')}>
|
<PageContent title={translate('Tasks')}>
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<ScheduledTasksConnector />
|
<ScheduledTasksConnector />
|
||||||
<QueuedTasksConnector />
|
<QueuedTasks />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||||
"lint-fix": "yarn lint --fix",
|
"lint-fix": "yarn lint --fix",
|
||||||
"stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
"stylelint": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/Sonarr/Sonarr",
|
"repository": "https://github.com/Sonarr/Sonarr",
|
||||||
"author": "Team Sonarr",
|
"author": "Team Sonarr",
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
|
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="4.7.14" />
|
<PackageReference Include="NLog" Version="4.7.14" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.8" />
|
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||||
|
|||||||
@@ -93,6 +93,30 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
|||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("{Custom Formats:-INTERNAL}", "AMZN NAME WITH SPACES")]
|
||||||
|
[TestCase("{Custom Formats:-NAME WITH SPACES}", "INTERNAL AMZN")]
|
||||||
|
[TestCase("{Custom Formats:-INTERNAL,NAME WITH SPACES}", "AMZN")]
|
||||||
|
[TestCase("{Custom Formats:INTERNAL}", "INTERNAL")]
|
||||||
|
[TestCase("{Custom Formats:NAME WITH SPACES}", "NAME WITH SPACES")]
|
||||||
|
[TestCase("{Custom Formats:INTERNAL,NAME WITH SPACES}", "INTERNAL NAME WITH SPACES")]
|
||||||
|
public void should_replace_custom_formats_with_filtered_names(string format, string expected)
|
||||||
|
{
|
||||||
|
_namingConfig.StandardEpisodeFormat = format;
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats)
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("{Custom Formats:-}", "{Custom Formats:-}")]
|
||||||
|
[TestCase("{Custom Formats:}", "{Custom Formats:}")]
|
||||||
|
public void should_not_replace_custom_formats_due_to_invalid_token(string format, string expected)
|
||||||
|
{
|
||||||
|
_namingConfig.StandardEpisodeFormat = format;
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats)
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase("{Custom Format}", "")]
|
[TestCase("{Custom Format}", "")]
|
||||||
[TestCase("{Custom Format:INTERNAL}", "INTERNAL")]
|
[TestCase("{Custom Format:INTERNAL}", "INTERNAL")]
|
||||||
[TestCase("{Custom Format:AMZN}", "AMZN")]
|
[TestCase("{Custom Format:AMZN}", "AMZN")]
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[Naruto-Kun.Hu] Dr Series S3 - 21 [1080p]", "Dr Series S3", 21, 0, 0)]
|
[TestCase("[Naruto-Kun.Hu] Dr Series S3 - 21 [1080p]", "Dr Series S3", 21, 0, 0)]
|
||||||
[TestCase("[Naruto-Kun.Hu] Series Title - 12 [1080p].mkv", "Series Title", 12, 0, 0)]
|
[TestCase("[Naruto-Kun.Hu] Series Title - 12 [1080p].mkv", "Series Title", 12, 0, 0)]
|
||||||
[TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)]
|
[TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)]
|
||||||
|
[TestCase("[Mystic Z-Team] Series Title Super - Episode 013 VF - Non-censuré [720p].mp4", "Series Title Super", 13, 0, 0)]
|
||||||
|
[TestCase("Series Title Kai Episodio 13 Audio Latino", "Series Title Kai", 13, 0, 0)]
|
||||||
|
|
||||||
// [TestCase("", "", 0, 0, 0)]
|
// [TestCase("", "", 0, 0, 0)]
|
||||||
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
||||||
|
|||||||
@@ -17,5 +17,22 @@
|
|||||||
"AddANewPath": "Tilføj en ny sti",
|
"AddANewPath": "Tilføj en ny sti",
|
||||||
"AddConditionImplementation": "Tilføj betingelse - {implementationName}",
|
"AddConditionImplementation": "Tilføj betingelse - {implementationName}",
|
||||||
"AddConnectionImplementation": "Tilføj forbindelse - {implementationName}",
|
"AddConnectionImplementation": "Tilføj forbindelse - {implementationName}",
|
||||||
"AddCustomFilter": "Tilføj tilpasset filter"
|
"AddCustomFilter": "Tilføj tilpasset filter",
|
||||||
|
"ApplyChanges": "Anvend ændringer",
|
||||||
|
"Test": "Afprøv",
|
||||||
|
"AddImportList": "Tilføj importliste",
|
||||||
|
"AddExclusion": "Tilføj undtagelse",
|
||||||
|
"TestAll": "Afprøv alle",
|
||||||
|
"TestAllClients": "Afprøv alle klienter",
|
||||||
|
"TestAllLists": "Afprøv alle lister",
|
||||||
|
"Unknown": "Ukendt",
|
||||||
|
"AllTitles": "All titler",
|
||||||
|
"TablePageSize": "Sidestørrelse",
|
||||||
|
"TestAllIndexers": "Afprøv alle indeks",
|
||||||
|
"AddDownloadClientImplementation": "Tilføj downloadklient - {implementationName}",
|
||||||
|
"AddIndexerError": "Kunne ikke tilføje en ny indekser. Prøv igen.",
|
||||||
|
"AddImportListImplementation": "Tilføj importliste - {implementationName}",
|
||||||
|
"AddRootFolderError": "Kunne ikke tilføje rodmappe",
|
||||||
|
"Table": "Tabel",
|
||||||
|
"AddIndexer": "Tilføj indekser"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2056,5 +2056,6 @@
|
|||||||
"NotificationsValidationInvalidApiKeyExceptionMessage": "Clave API inválida: {exceptionMessage}",
|
"NotificationsValidationInvalidApiKeyExceptionMessage": "Clave API inválida: {exceptionMessage}",
|
||||||
"NotificationsJoinSettingsApiKeyHelpText": "La clave API de tus ajustes de Añadir cuenta (haz clic en el botón Añadir API).",
|
"NotificationsJoinSettingsApiKeyHelpText": "La clave API de tus ajustes de Añadir cuenta (haz clic en el botón Añadir API).",
|
||||||
"NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivo (deja en blanco para enviar a todos los dispositivos)",
|
"NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivo (deja en blanco para enviar a todos los dispositivos)",
|
||||||
"NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')"
|
"NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
|
||||||
|
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2056,5 +2056,6 @@
|
|||||||
"MetadataSettingsSeriesMetadataUrl": "URL des métadonnées de la série",
|
"MetadataSettingsSeriesMetadataUrl": "URL des métadonnées de la série",
|
||||||
"NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise",
|
"NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise",
|
||||||
"DatabaseMigration": "Migration des bases de données",
|
"DatabaseMigration": "Migration des bases de données",
|
||||||
"Filters": "Filtres"
|
"Filters": "Filtres",
|
||||||
|
"ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2056,5 +2056,6 @@
|
|||||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Mover para o Diretório Quando Concluído",
|
"DownloadClientDelugeSettingsDirectoryCompleted": "Mover para o Diretório Quando Concluído",
|
||||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Local opcional para mover os downloads concluídos, deixe em branco para usar o local padrão do Deluge",
|
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Local opcional para mover os downloads concluídos, deixe em branco para usar o local padrão do Deluge",
|
||||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Deluge",
|
"DownloadClientDelugeSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Deluge",
|
||||||
"EpisodeRequested": "Episódio Pedido"
|
"EpisodeRequested": "Episódio Pedido",
|
||||||
|
"ReleaseProfileIndexerHelpTextWarning": "Definir um indexador específico em um perfil de lançamento fará com que esse perfil seja aplicado apenas a lançamentos desse indexador."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,5 +38,61 @@
|
|||||||
"AddList": "Додати список",
|
"AddList": "Додати список",
|
||||||
"AddListError": "Неможливо додати новий список, спробуйте ще раз.",
|
"AddListError": "Неможливо додати новий список, спробуйте ще раз.",
|
||||||
"AddListExclusionError": "Неможливо додати новий виняток зі списку, спробуйте ще раз.",
|
"AddListExclusionError": "Неможливо додати новий виняток зі списку, спробуйте ще раз.",
|
||||||
"AddListExclusionSeriesHelpText": "Заборонити додавання серіалів до {appName} зі списків"
|
"AddListExclusionSeriesHelpText": "Заборонити додавання серіалів до {appName} зі списків",
|
||||||
|
"AllSeriesAreHiddenByTheAppliedFilter": "Всі результати приховані фільтром",
|
||||||
|
"AlternateTitles": "Альтернативна назва",
|
||||||
|
"Analytics": "Аналітика",
|
||||||
|
"Apply": "Застосувати",
|
||||||
|
"ApplyTags": "Застосувати теги",
|
||||||
|
"ApplyTagsHelpTextAdd": "Додати: додати теги до наявного списку тегів",
|
||||||
|
"ApplyTagsHelpTextHowToApplyImportLists": "Як застосувати теги до вибраних списків імпорту",
|
||||||
|
"ApplyTagsHelpTextRemove": "Видалити: видалити введені теги",
|
||||||
|
"ApplyTagsHelpTextHowToApplyIndexers": "Як застосувати теги до вибраних індексаторів",
|
||||||
|
"ApplyTagsHelpTextReplace": "Замінити: Змінити наявні теги на введені теги (залишіть порожнім, щоб очистити всі теги)",
|
||||||
|
"AuthenticationMethodHelpTextWarning": "Виберіть дійсний метод автентифікації",
|
||||||
|
"AirsDateAtTimeOn": "{date} о {time} на {networkLabel}",
|
||||||
|
"AirDate": "Дата трансляції",
|
||||||
|
"AddRemotePathMapping": "Додати віддалений шлях",
|
||||||
|
"AddRemotePathMappingError": "Не вдалося додати нове зіставлення віддаленого шляху, спробуйте ще раз.",
|
||||||
|
"AnalyticsEnabledHelpText": "Надсилайте анонімну інформацію про використання та помилки на сервери {appName}. Це включає інформацію про ваш веб-переглядач, які сторінки {appName} WebUI ви використовуєте, звіти про помилки, а також версію ОС і часу виконання. Ми будемо використовувати цю інформацію, щоб визначити пріоритети функцій і виправлення помилок.",
|
||||||
|
"ApiKeyValidationHealthCheckMessage": "Будь ласка оновіть ключ API, щоб він містив принаймні {length} символів. Ви можете зробити це в налаштуваннях або в файлі конфігурації",
|
||||||
|
"AppDataLocationHealthCheckMessage": "Оновлення буде неможливим, щоб запобігти видаленню AppData під час оновлення",
|
||||||
|
"ApplyTagsHelpTextHowToApplyDownloadClients": "Як застосувати теги до вибраних клієнтів завантаження",
|
||||||
|
"AllResultsAreHiddenByTheAppliedFilter": "Всі результати приховані фільтром",
|
||||||
|
"AudioInfo": "Аудіо інформація",
|
||||||
|
"Age": "Вік",
|
||||||
|
"All": "Всі",
|
||||||
|
"Anime": "Аніме",
|
||||||
|
"AgeWhenGrabbed": "Вік (коли схоплено)",
|
||||||
|
"AnimeEpisodeFormat": "Формат серії аніме",
|
||||||
|
"ApiKey": "API Ключ",
|
||||||
|
"ApplicationURL": "URL програми",
|
||||||
|
"AppDataDirectory": "Каталог AppData",
|
||||||
|
"AptUpdater": "Використовуйте apt для інсталяції оновлення",
|
||||||
|
"AddRootFolder": "Додати корневий каталог",
|
||||||
|
"AllTitles": "Усі Назви",
|
||||||
|
"Always": "Завжди",
|
||||||
|
"AddNewSeriesError": "Не вдалося завантажити результати пошуку, спробуйте ще.",
|
||||||
|
"AlreadyInYourLibrary": "Вже у вашій бібліотеці",
|
||||||
|
"AddDelayProfileError": "Неможливо додати новий профіль затримки, будь ласка спробуйте ще.",
|
||||||
|
"AddNewSeriesHelpText": "Додати новий серіал легко, просто почніть вводити назву серіалу, який ви хочете додати.",
|
||||||
|
"AddNewSeriesRootFolderHelpText": "Підпапка '{folder}' буде створена автоматично",
|
||||||
|
"AddNewSeriesSearchForMissingEpisodes": "Почніть пошук відсутніх епізодів",
|
||||||
|
"AddNotificationError": "Не вдалося додати нове сповіщення, спробуйте ще раз.",
|
||||||
|
"AddQualityProfile": "Додати профіль якості",
|
||||||
|
"AddQualityProfileError": "Не вдалося додати новий профіль якості, спробуйте ще раз.",
|
||||||
|
"AddReleaseProfile": "Додати профіль релізу",
|
||||||
|
"AirsTimeOn": "{time} на {networkLabel}",
|
||||||
|
"AllFiles": "Всі файли",
|
||||||
|
"AirsTomorrowOn": "Завтра о {time} на {networkLabel}",
|
||||||
|
"AnalyseVideoFiles": "Аналізувати відео файли",
|
||||||
|
"AnalyseVideoFilesHelpText": "Отримайте з файлів інформацію про відео, таку як роздільна здатність, час виконання та кодек. Це вимагає, щоб {appName} читав частини файлу, що може спричинити високу дискову або мережеву активність під час сканування.",
|
||||||
|
"Any": "Будь-який",
|
||||||
|
"AppUpdated": "{appName} Оновлено",
|
||||||
|
"ApplicationUrlHelpText": "Зовнішня URL-адреса цієї програми, включаючи http(s)://, порт і базу URL-адрес",
|
||||||
|
"ApplyChanges": "Застосувати зміни",
|
||||||
|
"AudioLanguages": "Мови аудіо",
|
||||||
|
"AuthForm": "Форми (сторінка входу)",
|
||||||
|
"Authentication": "Автентифікація",
|
||||||
|
"AuthenticationMethod": "Метод автентифікації"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Organizer
|
|||||||
private readonly ICached<bool> _patternHasEpisodeIdentifierCache;
|
private readonly ICached<bool> _patternHasEpisodeIdentifierCache;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}",
|
private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ ,a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
|
private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
|
||||||
@@ -698,7 +698,7 @@ namespace NzbDrone.Core.Organizer
|
|||||||
customFormats = _formatCalculator.ParseCustomFormat(episodeFile, series);
|
customFormats = _formatCalculator.ParseCustomFormat(episodeFile, series);
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming));
|
tokenHandlers["{Custom Formats}"] = m => GetCustomFormatsToken(customFormats, m.CustomFormat);
|
||||||
tokenHandlers["{Custom Format}"] = m =>
|
tokenHandlers["{Custom Format}"] = m =>
|
||||||
{
|
{
|
||||||
if (m.CustomFormat.IsNullOrWhiteSpace())
|
if (m.CustomFormat.IsNullOrWhiteSpace())
|
||||||
@@ -717,6 +717,29 @@ namespace NzbDrone.Core.Organizer
|
|||||||
tokenHandlers["{TvMazeId}"] = m => series.TvMazeId > 0 ? series.TvMazeId.ToString() : string.Empty;
|
tokenHandlers["{TvMazeId}"] = m => series.TvMazeId > 0 ? series.TvMazeId.ToString() : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter)
|
||||||
|
{
|
||||||
|
var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming);
|
||||||
|
|
||||||
|
var filteredTokens = tokens;
|
||||||
|
|
||||||
|
if (filter.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
if (filter.StartsWith("-"))
|
||||||
|
{
|
||||||
|
var splitFilter = filter.Substring(1).Split(',');
|
||||||
|
filteredTokens = tokens.Where(c => !splitFilter.Contains(c.Name)).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var splitFilter = filter.Split(',');
|
||||||
|
filteredTokens = tokens.Where(c => splitFilter.Contains(c.Name)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", filteredTokens);
|
||||||
|
}
|
||||||
|
|
||||||
private string GetLanguagesToken(List<string> mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted)
|
private string GetLanguagesToken(List<string> mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted)
|
||||||
{
|
{
|
||||||
var tokens = new List<string>();
|
var tokens = new List<string>();
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Parser
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
// Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01)
|
// Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01)
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
// Anime - [SubGroup] Title Absolute Episode Number + Season+Episode
|
// Anime - [SubGroup] Title Absolute Episode Number + Season+Episode
|
||||||
@@ -401,6 +401,10 @@ namespace NzbDrone.Core.Parser
|
|||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>[(\[]\w{8}[)\]])?$",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>[(\[]\w{8}[)\]])?$",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
|
// Anime - Title Episode/Episodio Absolute Episode Number
|
||||||
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+(?:Episode|Episodio)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
// Anime - Title Absolute Episode Number
|
// Anime - Title Absolute Episode Number
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
|
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
|
||||||
<PackageReference Include="MailKit" Version="3.6.0" />
|
<PackageReference Include="MailKit" Version="3.6.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.21" />
|
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.21" />
|
||||||
<PackageReference Include="Polly" Version="8.2.0" />
|
<PackageReference Include="Polly" Version="8.3.1" />
|
||||||
<PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" />
|
<PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" />
|
||||||
<PackageReference Include="Servarr.FFprobe" Version="5.1.4.112" />
|
<PackageReference Include="Servarr.FFprobe" Version="5.1.4.112" />
|
||||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||||
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="4.7.14" />
|
<PackageReference Include="NLog" Version="4.7.14" />
|
||||||
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.8" />
|
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||||
<PackageReference Include="Npgsql" Version="7.0.4" />
|
<PackageReference Include="Npgsql" Version="7.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
|
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
|
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
<PackageReference Include="NLog" Version="4.7.14" />
|
<PackageReference Include="NLog" Version="4.7.14" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ namespace Sonarr.Api.V3.Indexers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
||||||
PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty();
|
PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty().When(s => s.MagnetUrl.IsNullOrWhiteSpace());
|
||||||
|
PostValidator.RuleFor(s => s.MagnetUrl).NotEmpty().When(s => s.DownloadUrl.IsNullOrWhiteSpace());
|
||||||
PostValidator.RuleFor(s => s.Protocol).NotEmpty();
|
PostValidator.RuleFor(s => s.Protocol).NotEmpty();
|
||||||
PostValidator.RuleFor(s => s.PublishDate).NotEmpty();
|
PostValidator.RuleFor(s => s.PublishDate).NotEmpty();
|
||||||
}
|
}
|
||||||
@@ -50,7 +51,7 @@ namespace Sonarr.Api.V3.Indexers
|
|||||||
[Consumes("application/json")]
|
[Consumes("application/json")]
|
||||||
public ActionResult<List<ReleaseResource>> Create(ReleaseResource release)
|
public ActionResult<List<ReleaseResource>> Create(ReleaseResource release)
|
||||||
{
|
{
|
||||||
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl);
|
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl ?? release.MagnetUrl);
|
||||||
|
|
||||||
ValidateResource(release);
|
ValidateResource(release);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user