1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-16 21:15:28 -04:00

Compare commits

...

17 Commits

Author SHA1 Message Date
Weblate
cf3d51bab2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Casselluu <jack10193@163.com>
Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: infoaitek24 <info@aitekph.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: shimmyx <shimmygodx@gmail.com>
Co-authored-by: vfaergestad <vgf@hotmail.no>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-03-21 21:21:30 -07:00
Mark McDowall
dec3fc6889 Fixed: Don't add series from import list with no matched TVDB ID 2024-03-22 00:21:04 -04:00
Mark McDowall
40bac23698 New: Support parsing season number from season folder when importing
Closes #903
2024-03-21 21:20:49 -07:00
Mark McDowall
88de927435 Fixed: Plex Watchlist import list 2024-03-21 21:20:27 -07:00
Mark McDowall
29204c93a3 New: Parsing multi-episode file with two and three digit episode numbers
Closes #6631
2024-03-21 21:20:13 -07:00
Mark McDowall
c641733781 Fixed: Task progress messages in the UI
Closes #6632
2024-03-21 21:20:08 -07:00
Weblate
58de0310fd Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: infoaitek24 <info@aitekph.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: vfaergestad <vgf@hotmail.no>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/
Translation: Servarr/Sonarr
2024-03-21 21:20:01 -07:00
Bogdan
172b1a82d1 Sort series by title in task name 2024-03-21 21:19:23 -07:00
Bogdan
e14568adef Ensure not allowed cursor is shown for disabled select inputs 2024-03-21 21:19:23 -07:00
Weblate
381ce61aef Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dennis Langthjem <dennis@langthjem.dk>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ihor Mudryi <mudryy33@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-03-13 21:49:22 -07:00
Mark McDowall
9f705e4161 Fixed: Release push with only Magnet URL
Closes #6622
2024-03-13 21:47:50 -07:00
Mark McDowall
063dba22a8 Fixed: Disabled select option still selectable 2024-03-13 21:47:33 -07:00
Mark McDowall
6d552f2a60 New: Show Series title and season number after task name when applicable
Closes #6601
2024-03-13 21:47:22 -07:00
Mark McDowall
4d4d63921b Add notification for build success/failures 2024-03-14 00:47:01 -04:00
Alan Collins
6584d95331 New: Update Custom Format renaming token to allow excluding specific formats
Closes #6615
2024-03-14 00:46:33 -04:00
Bogdan
86034beccd Bump ImageSharp, Polly, DryIoc, STJson, WindowsServices 2024-03-13 21:44:23 -07:00
Mark McDowall
4aa56e3f91 Fixed: Parsing of some French and Spanish anime releases 2024-03-13 21:44:07 -07:00
58 changed files with 755 additions and 541 deletions

View File

@@ -121,7 +121,7 @@ jobs:
run: yarn lint
- name: Stylelint
run: yarn stylelint
run: yarn stylelint -f github
- name: Build
run: yarn build --env production
@@ -225,3 +225,25 @@ jobs:
branch: ${{ github.ref_name }}
major_version: ${{ needs.backend.outputs.major_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' }}

View File

@@ -13,6 +13,8 @@ export interface CommandBody {
trigger: string;
suppressMessages: boolean;
seriesId?: number;
seriesIds?: number[];
seasonNumber?: number;
}
interface Command extends ModelBase {

View File

@@ -19,7 +19,7 @@
.isDisabled {
opacity: 0.7;
cursor: not-allowed;
cursor: not-allowed !important;
}
.dropdownArrowContainer {

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import monitorOptions from 'Utilities/Series/monitorOptions';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
function MonitorEpisodesSelectInput(props) {
const {
@@ -19,7 +19,7 @@ function MonitorEpisodesSelectInput(props) {
get value() {
return translate('NoChange');
},
disabled: true
isDisabled: true
});
}
@@ -29,12 +29,12 @@ function MonitorEpisodesSelectInput(props) {
get value() {
return `(${translate('Mixed')})`;
},
disabled: true
isDisabled: true
});
}
return (
<SelectInput
<EnhancedSelectInput
values={values}
{...otherProps}
/>

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
import SelectInput from './SelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
function MonitorNewItemsSelectInput(props) {
const {
@@ -16,7 +16,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({
key: 'noChange',
value: 'No Change',
disabled: true
isDisabled: true
});
}
@@ -24,12 +24,12 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}
return (
<SelectInput
<EnhancedSelectInput
values={values}
{...otherProps}
/>

View File

@@ -28,7 +28,7 @@ function createMapStateToProps() {
get value() {
return translate('NoChange');
},
disabled: includeNoChangeDisabled
isDisabled: includeNoChangeDisabled
});
}
@@ -38,7 +38,7 @@ function createMapStateToProps() {
get value() {
return `(${translate('Mixed')})`;
},
disabled: true
isDisabled: true
});
}

View File

@@ -15,7 +15,7 @@ interface ISeriesTypeOption {
key: string;
value: string;
format?: string;
disabled?: boolean;
isDisabled?: boolean;
}
const seriesTypeOptions: ISeriesTypeOption[] = [
@@ -55,7 +55,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: includeNoChangeDisabled,
isDisabled: includeNoChangeDisabled,
});
}
@@ -63,7 +63,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
values.unshift({
key: 'mixed',
value: `(${translate('Mixed')})`,
disabled: true,
isDisabled: true,
});
}

View File

@@ -36,7 +36,7 @@ const monitoredOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'monitored',
@@ -58,7 +58,7 @@ const seasonFolderOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'yes',

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -31,7 +31,7 @@ const autoAddOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -26,7 +26,7 @@
.token {
flex: 0 0 50%;
padding: 6px 6px;
padding: 6px;
background-color: var(--popoverTitleBackgroundColor);
font-family: $monoSpaceFontFamily;
}
@@ -36,7 +36,7 @@
align-items: center;
justify-content: space-between;
flex: 0 0 50%;
padding: 6px 6px;
padding: 6px;
background-color: var(--popoverBodyBackgroundColor);
.footNote {

View File

@@ -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;

View File

@@ -10,15 +10,6 @@
width: 100%;
}
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}
.queued,
.started,
.ended {

View File

@@ -2,14 +2,12 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'commandName': string;
'duration': string;
'ended': string;
'queued': string;
'started': string;
'trigger': string;
'triggerContent': string;
'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -0,0 +1,8 @@
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}

View File

@@ -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;

View File

@@ -0,0 +1,57 @@
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));
const sortedSeries = series.sort((a, b) =>
a.sortTitle.localeCompare(b.sortTitle)
);
return (
<TableRowCell>
<span className={styles.commandName}>
{commandName}
{sortedSeries.length ? (
<span> - {sortedSeries.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>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate';
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
import QueuedTasks from './Queued/QueuedTasks';
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
function Tasks() {
@@ -10,7 +10,7 @@ function Tasks() {
<PageContent title={translate('Tasks')}>
<PageContentBody>
<ScheduledTasksConnector />
<QueuedTasksConnector />
<QueuedTasks />
</PageContentBody>
</PageContent>
);

View File

@@ -10,7 +10,7 @@
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
"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",
"author": "Team Sonarr",

View File

@@ -4,16 +4,16 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<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.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="NLog" Version="4.7.14" />
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Sentry" Version="4.0.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.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />

View File

@@ -120,6 +120,17 @@ namespace NzbDrone.Core.Test.ImportListTests
private void WithImdbId()
{
_list1Series.First().ImdbId = "tt0496424";
Mocker.GetMock<ISearchForNewSeries>()
.Setup(s => s.SearchForNewSeriesByImdbId(_list1Series.First().ImdbId))
.Returns(
Builder<Series>
.CreateListOfSize(1)
.All()
.With(s => s.Title = "Breaking Bad")
.With(s => s.TvdbId = 81189)
.Build()
.ToList());
}
private void WithExistingSeries()
@@ -342,6 +353,7 @@ namespace NzbDrone.Core.Test.ImportListTests
public void should_add_new_series_from_single_list_to_library()
{
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
WithTvdbId();
WithList(1, true);
WithCleanLevel(ListSyncLevelType.Disabled);
@@ -358,6 +370,7 @@ namespace NzbDrone.Core.Test.ImportListTests
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
_importListFetch.Series.AddRange(_list2Series);
WithTvdbId();
WithList(1, true);
WithList(2, true);
@@ -376,6 +389,7 @@ namespace NzbDrone.Core.Test.ImportListTests
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
_importListFetch.Series.AddRange(_list2Series);
WithTvdbId();
WithList(1, true);
WithList(2, false);
@@ -422,12 +436,17 @@ namespace NzbDrone.Core.Test.ImportListTests
public void should_search_by_imdb_if_series_title_and_series_imdb()
{
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
WithList(1, true);
WithImdbId();
Subject.Execute(_commandAll);
Mocker.GetMock<ISearchForNewSeries>()
.Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()), Times.Once());
Mocker.GetMock<IAddSeriesService>()
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1), It.IsAny<bool>()));
}
[Test]
@@ -498,5 +517,18 @@ namespace NzbDrone.Core.Test.ImportListTests
Mocker.GetMock<IImportListExclusionService>()
.Verify(v => v.All(), Times.Never);
}
[Test]
public void should_not_add_if_tvdbid_is_0()
{
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
WithList(1, true);
WithExcludedSeries();
Subject.Execute(_commandAll);
Mocker.GetMock<IAddSeriesService>()
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>()));
}
}
}

View File

@@ -93,6 +93,30 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.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:INTERNAL}", "INTERNAL")]
[TestCase("{Custom Format:AMZN}", "AMZN")]

View File

@@ -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] 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("[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)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)

View File

@@ -77,6 +77,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title (S15E06-08) City Sushi", "Series Title", 15, new[] { 6, 7, 8 })]
[TestCase("Босх: Спадок (S2E1-4) / Series: Legacy (S2E1-4) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })]
[TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })]
[TestCase("Series Title - S26E96-97-98-99-100 - Episode 5931 + Episode 5932 + Episode 5933 + Episode 5934 + Episode 5935", "Series Title", 26, new[] { 96, 97, 98, 99, 100 })]
// [TestCase("", "", , new [] { })]
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)

View File

@@ -29,6 +29,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase(@"C:\Test\Series\Season 01\1 Pilot (1080p HD).mkv", 1, 1)]
[TestCase(@"C:\Test\Series\Season 1\02 Honor Thy Father (1080p HD).m4v", 1, 2)]
[TestCase(@"C:\Test\Series\Season 1\2 Honor Thy Developer (1080p HD).m4v", 1, 2)]
[TestCase(@"C:\Test\Series\Season 2 - Total Series Action\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
[TestCase(@"C:\Test\Series\Season 2\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
// [TestCase(@"C:\series.state.S02E04.720p.WEB-DL.DD5.1.H.264\73696S02-04.mkv", 2, 4)] //Gets treated as S01E04 (because it gets parsed as anime); 2020-01 broken test case: Expected result.EpisodeNumbers to contain 1 item(s), but found 0
public void should_parse_from_path(string path, int season, int episode)
@@ -45,6 +47,7 @@ namespace NzbDrone.Core.Test.ParserTests
}
[TestCase("01-03\\The Series Title (2010) - 1x01-02-03 - Episode Title HDTV-720p Proper", "The Series Title (2010)", 1, new[] { 1, 2, 3 })]
[TestCase("Season 2\\E05-06 - Episode Title HDTV-720p Proper", "", 2, new[] { 5, 6 })]
public void should_parse_multi_episode_from_path(string path, string title, int season, int[] episodes)
{
var result = Parser.Parser.ParsePath(path.AsOsAgnostic());

View File

@@ -190,6 +190,12 @@ namespace NzbDrone.Core.ImportLists
item.Title = mappedSeries.Title;
}
if (item.TvdbId == 0)
{
_logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title);
continue;
}
// Check to see if series excluded
var excludedSeries = listExclusions.Where(s => s.TvdbId == item.TvdbId).SingleOrDefault();
@@ -202,7 +208,7 @@ namespace NzbDrone.Core.ImportLists
// Break if Series Exists in DB
if (existingTvdbIds.Any(x => x == item.TvdbId))
{
_logger.Debug("{0} [{1}] Rejected, Series Exists in DB", item.TvdbId, item.Title);
_logger.Debug("{0} [{1}] Rejected, series exists in database", item.TvdbId, item.Title);
continue;
}

View File

@@ -522,7 +522,7 @@ namespace NzbDrone.Core.IndexerSearch
var reports = batch.SelectMany(x => x).ToList();
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
_logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
// Update the last search time for all episodes if at least 1 indexer was searched.
if (indexers.Any())

View File

@@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers
public class RssSyncCommand : Command
{
public override bool SendUpdatesToClient => true;
public override bool IsLongRunning => true;
}
}

View File

@@ -665,5 +665,13 @@
"NotificationsPushoverSettingsRetry": "Torna-ho a provar",
"NotificationsSettingsWebhookMethod": "Mètode",
"Other": "Altres",
"Monitor": "Monitora"
"Monitor": "Monitora",
"AutoTaggingSpecificationOriginalLanguage": "Llenguatge",
"AutoTaggingSpecificationQualityProfile": "Perfil de Qualitat",
"AutoTaggingSpecificationRootFolder": "Carpeta arrel",
"AddDelayProfileError": "No s'ha pogut afegir un perfil realentit, torna-ho a probar",
"AutoTaggingSpecificationSeriesType": "Tipus de Sèries",
"AutoTaggingSpecificationStatus": "Estat",
"BlocklistAndSearch": "Llista de bloqueig i cerca",
"BlocklistAndSearchHint": "Comença una cerca per reemplaçar després d'haver bloquejat"
}

View File

@@ -17,5 +17,22 @@
"AddANewPath": "Tilføj en ny sti",
"AddConditionImplementation": "Tilføj betingelse - {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"
}

View File

@@ -41,7 +41,7 @@
"SkipFreeSpaceCheck": "Prüfung des freien Speichers überspringen",
"AbsoluteEpisodeNumber": "Exakte Folgennummer",
"AddConnection": "Verbindung hinzufügen",
"AddAutoTagError": "Der neue automatische Tag konnte nicht hinzugefügt werden, bitte versuche es erneut.",
"AddAutoTagError": "Auto-Tag konnte nicht hinzugefügt werden. Bitte erneut versuchen.",
"AddConditionError": "Neue Bedingung konnte nicht hinzugefügt werden, bitte erneut versuchen.",
"AddCustomFormat": "Eigenes Format hinzufügen",
"AddCustomFormatError": "Neues eigenes Format kann nicht hinzugefügt werden, bitte versuchen Sie es erneut.",
@@ -146,7 +146,7 @@
"AuthenticationRequiredHelpText": "Ändern, welche anfragen Authentifizierung benötigen. Ändere nichts wenn du dir nicht des Risikos bewusst bist.",
"AnalyseVideoFilesHelpText": "Videoinformationen wie Auflösung, Laufzeit und Codec-Informationen aus Dateien extrahieren. Dies erfordert, dass {appName} Teile der Datei liest, was bei Scans zu hoher Festplatten- oder Netzwerkaktivität führen kann.",
"AnalyticsEnabledHelpText": "Senden Sie anonyme Nutzungs- und Fehlerinformationen an die Server von {appName}. Dazu gehören Informationen zu Ihrem Browser, welche {appName}-WebUI-Seiten Sie verwenden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.",
"AutoTaggingNegateHelpText": "Wenn diese Option aktiviert ist, wird die automatische Tagging-Regel nicht angewendet, wenn diese {implementationName}-Bedingung zutrifft.",
"AutoTaggingNegateHelpText": "Falls aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.",
"CopyUsingHardlinksSeriesHelpText": "Mithilfe von Hardlinks kann {appName} Seeding-Torrents in den Serienordner importieren, ohne zusätzlichen Speicherplatz zu beanspruchen oder den gesamten Inhalt der Datei zu kopieren. Hardlinks funktionieren nur, wenn sich Quelle und Ziel auf demselben Volume befinden",
"DailyEpisodeTypeFormat": "Datum ({format})",
"DefaultDelayProfileSeries": "Dies ist das Standardprofil. Es gilt für alle Serien, die kein explizites Profil haben.",
@@ -171,7 +171,7 @@
"BackupIntervalHelpText": "Intervall zwischen automatischen Sicherungen",
"BuiltIn": "Eingebaut",
"ChangeFileDate": "Ändern Sie das Dateidatum",
"CustomFormatsLoadError": "Benutzerdefinierte Formate können nicht geladen werden",
"CustomFormatsLoadError": "Eigene Formate konnten nicht geladen werden",
"DeleteQualityProfileMessageText": "Sind Sie sicher, dass Sie das Qualitätsprofil „{name}“ löschen möchten?",
"DeletedReasonUpgrade": "Die Datei wurde gelöscht, um ein Upgrade zu importieren",
"DeleteEpisodesFiles": "{episodeFileCount} Episodendateien löschen",
@@ -205,7 +205,7 @@
"AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich",
"Automatic": "Automatisch",
"AutomaticSearch": "Automatische Suche",
"AutoTaggingRequiredHelpText": "Diese {implementationName}-Bedingung muss zutreffen, damit die automatische Tagging-Regel angewendet wird. Andernfalls reicht eine einzelne {implementationName}-Übereinstimmung aus.",
"AutoTaggingRequiredHelpText": "Diese {0} Bedingungen müssen erfüllt sein, damit das eigene Format zutrifft. Ansonsten reicht ein einzelner {1} Treffer.",
"BackupRetentionHelpText": "Automatische Backups, die älter als der Aufbewahrungszeitraum sind, werden automatisch bereinigt",
"BindAddressHelpText": "Gültige IP-Adresse, localhost oder „*“ für alle Schnittstellen",
"BackupsLoadError": "Sicherrungen können nicht geladen werden",
@@ -280,8 +280,8 @@
"Custom": "Benutzerdefiniert",
"CustomFilters": "Benutzerdefinierte Filter",
"CustomFormat": "Benutzerdefiniertes Format",
"CustomFormats": "Benutzerdefinierte Formate",
"CustomFormatsSettingsSummary": "Benutzerdefinierte Formate und Einstellungen",
"CustomFormats": "Eigene Formate",
"CustomFormatsSettingsSummary": "Eigene Formate und Einstellungen",
"DailyEpisodeFormat": "Tägliches Episodenformat",
"Database": "Datenbank",
"Dates": "Termine",
@@ -540,7 +540,7 @@
"ApplyTagsHelpTextAdd": "Hinzufügen: Fügen Sie die Tags der vorhandenen Tag-Liste hinzu",
"ApplyTagsHelpTextRemove": "Entfernen: Die eingegebenen Tags entfernen",
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetzen Sie die Tags durch die eingegebenen Tags (geben Sie keine Tags ein, um alle Tags zu löschen).",
"Wanted": " Gesucht",
"Wanted": "Gesucht",
"ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.",
"Continuing": "Fortsetzung",
"CopyUsingHardlinksHelpTextWarning": "Gelegentlich können Dateisperren das Umbenennen von Dateien verhindern, die geseedet werden. Sie können das Seeding vorübergehend deaktivieren und als Workaround die Umbenennungsfunktion von {appName} verwenden.",
@@ -550,7 +550,7 @@
"CountIndexersSelected": "{count} Indexer ausgewählt",
"CountSelectedFiles": "{selectedCount} ausgewählte Dateien",
"CustomFormatUnknownConditionOption": "Unbekannte Option „{key}“ für Bedingung „{implementation}“",
"CustomFormatsSettings": "Benutzerdefinierte Formateinstellungen",
"CustomFormatsSettings": "Einstellungen für eigene Formate",
"Daily": "Täglich",
"Dash": "Bindestrich",
"Debug": "Debuggen",
@@ -709,7 +709,7 @@
"ClickToChangeSeason": "Klicken Sie hier, um die Staffel zu ändern",
"BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert",
"BlackholeWatchFolder": "Überwachter Ordner",
"BlackholeWatchFolderHelpText": "Ordner, aus dem {appName} abgeschlossene Downloads importieren soll",
"BlackholeWatchFolderHelpText": "Der Ordner, aus dem {appName} fertige Downloads importieren soll",
"BrowserReloadRequired": "Neuladen des Browsers erforderlich",
"CalendarOptions": "Kalenderoptionen",
"CancelPendingTask": "Möchten Sie diese ausstehende Aufgabe wirklich abbrechen?",
@@ -776,5 +776,16 @@
"Airs": "Wird ausgestrahlt",
"AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden",
"IconForCutoffUnmet": "Symbol für Schwelle nicht erreicht",
"DownloadClientSettingsAddPaused": "Pausiert hinzufügen"
"DownloadClientSettingsAddPaused": "Pausiert hinzufügen",
"ClickToChangeIndexerFlags": "Klicken, um Indexer-Flags zu ändern",
"BranchUpdate": "Branch, der verwendet werden soll, um {appName} zu updaten",
"BlocklistAndSearch": "Sperrliste und Suche",
"AddDelayProfileError": "Verzögerungsprofil konnte nicht hinzugefügt werden. Bitte erneut versuchen.",
"BlocklistAndSearchHint": "Starte Suche nach einer Alternative, falls es der Sperrliste hinzugefügt wurde",
"BlocklistAndSearchMultipleHint": "Starte Suchen nach einer Alternative, falls es der Sperrliste hinzugefügt wurde",
"BlocklistMultipleOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternativen zu suchen",
"BlocklistOnly": "Nur der Sperrliste hinzufügen",
"BlocklistOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternative zu suchen",
"BlocklistReleaseHelpText": "Dieses Release für erneuten Download durch {appName} via RSS oder automatische Suche sperren",
"ChangeCategory": "Kategorie wechseln"
}

View File

@@ -2056,5 +2056,6 @@
"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).",
"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."
}

View File

@@ -245,7 +245,7 @@
"SubtitleLanguages": "Langues des sous-titres",
"Clone": "Dupliquer",
"ColonReplacementFormatHelpText": "Changer la manière dont {appName} remplace les « deux-points »",
"DefaultCase": "Casse par défaut",
"DefaultCase": "Case par défaut",
"Delete": "Supprimer",
"DelayProfiles": "Profils de retard",
"DelayProfilesLoadError": "Impossible de charger les profils de retard",
@@ -1281,8 +1281,8 @@
"CreateEmptySeriesFolders": "Créer des dossiers de séries vides",
"Custom": "Customisé",
"CopyUsingHardlinksSeriesHelpText": "Les liens physiques permettent à {appName} d'importer des torrents dans le dossier de la série sans prendre d'espace disque supplémentaire ni copier l'intégralité du contenu du fichier. Les liens physiques ne fonctionneront que si la source et la destination sont sur le même volume",
"CustomFormatsSettingsSummary": "Paramètres de formats personnalisés",
"CustomFormatsSettings": "Paramètres de formats personnalisés",
"CustomFormatsSettingsSummary": "Formats et paramètres personnalisés",
"CustomFormatsSettings": "Paramètre des formats personnalisés",
"DefaultDelayProfileSeries": "Il s'agit du profil par défaut. Cela s'applique à toutes les séries qui n'ont pas de profil explicite.",
"DeleteDownloadClient": "Supprimer le client de téléchargement",
"DeleteEmptyFolders": "Supprimer les dossiers vides",
@@ -1355,7 +1355,7 @@
"DownloadClientStatusAllClientHealthCheckMessage": "Tous les clients de téléchargement sont indisponibles en raison d'échecs",
"DownloadClientsLoadError": "Impossible de charger les clients de téléchargement",
"DownloadPropersAndRepacks": "Propriétés et reconditionnements",
"DownloadClientsSettingsSummary": "Clients de téléchargement, gestion des téléchargements et mappages de chemins distants",
"DownloadClientsSettingsSummary": "Clients de téléchargement, gestion des téléchargements et mappages de chemins d'accès à distance",
"DownloadPropersAndRepacksHelpText": "S'il faut ou non mettre à niveau automatiquement vers Propers/Repacks",
"DownloadPropersAndRepacksHelpTextWarning": "Utilisez des formats personnalisés pour les mises à niveau automatiques vers Propers/Repacks",
"DownloadPropersAndRepacksHelpTextCustomFormat": "Utilisez « Ne pas préférer » pour trier par score de format personnalisé sur Propers/Repacks",
@@ -1415,7 +1415,7 @@
"EpisodesLoadError": "Impossible de charger les épisodes",
"Files": "Fichiers",
"Continuing": "Continuer",
"Donate": "Faire un don",
"Donate": "Donation",
"EditConditionImplementation": "Modifier la condition {implementationName}",
"EditConnectionImplementation": "Modifier la connexion - {implementationName}",
"EditImportListImplementation": "Modifier la liste d'importation - {implementationName}",
@@ -1931,8 +1931,8 @@
"DownloadClientDelugeSettingsDirectoryHelpText": "Emplacement dans lequel placer les téléchargements (facultatif), laissez vide pour utiliser l'emplacement Deluge par défaut",
"DownloadClientDelugeSettingsDirectory": "Dossier de téléchargement",
"DownloadClientDelugeSettingsDirectoryCompleted": "Dossier de déplacement une fois terminé",
"ClickToChangeIndexerFlags": "Cliquer pour changer les attributs de l'indexer",
"CustomFormatsSpecificationFlag": "Attribut",
"ClickToChangeIndexerFlags": "Cliquez pour changer les drapeaux de l'indexeur",
"CustomFormatsSpecificationFlag": "Drapeau",
"CustomFilter": "Filtre personnalisé",
"ImportListsTraktSettingsAuthenticateWithTrakt": "S'authentifier avec Trakt",
"SelectIndexerFlags": "Sélectionner les drapeaux de l'indexeur",
@@ -2056,5 +2056,6 @@
"MetadataSettingsSeriesMetadataUrl": "URL des métadonnées de la série",
"NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise",
"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."
}

View File

@@ -110,7 +110,7 @@
"AddAutoTagError": "Impossibile aggiungere un nuovo tag automatico, riprova.",
"AddCustomFormat": "Aggiungi Formato Personalizzato",
"AddDownloadClient": "Aggiungi Client di Download",
"AddCustomFormatError": "Non riesco ad aggiungere un nuovo formato personalizzato, riprova.",
"AddCustomFormatError": "Impossibile aggiungere un nuovo formato personalizzato, riprova.",
"AddDownloadClientError": "Impossibile aggiungere un nuovo client di download, riprova.",
"AddDelayProfile": "Aggiungi Profilo di Ritardo",
"AddIndexerError": "Impossibile aggiungere un nuovo Indicizzatore, riprova.",
@@ -248,5 +248,6 @@
"AnimeEpisodeTypeDescription": "Episodi rilasciati utilizzando un numero di episodio assoluto",
"AnimeEpisodeTypeFormat": "Numero assoluto dell'episodio ({format})",
"AutoRedownloadFailed": "Download fallito",
"AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova."
"AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.",
"Cutoff": "Taglio"
}

View File

@@ -8,5 +8,12 @@
"Absolute": "Absolutt",
"Activity": "Aktivitet",
"About": "Om",
"CalendarOptions": "Kalenderinnstillinger"
"CalendarOptions": "Kalenderinnstillinger",
"AbsoluteEpisodeNumbers": "Absolutte Episode Numre",
"AddANewPath": "Legg til ny filsti",
"AddConditionImplementation": "Legg til betingelse - {implementationName}",
"AddConditionError": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen",
"AbsoluteEpisodeNumber": "Absolutt Episode Nummer",
"AddAutoTagError": "Ikke mulig å legge til ny automatisk tagg, vennligst prøv igjen",
"Actions": "Handlinger"
}

View File

@@ -2056,5 +2056,6 @@
"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",
"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."
}

View File

@@ -38,5 +38,61 @@
"AddList": "Додати список",
"AddListError": "Неможливо додати новий список, спробуйте ще раз.",
"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": "Метод автентифікації"
}

View File

@@ -1786,7 +1786,7 @@
"DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置",
"DownloadClientPriorityHelpText": "下载客户端优先级从1最高到50最低默认为1。具有相同优先级的客户端将轮换使用。",
"IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值",
"ChangeCategory": "改分类",
"ChangeCategory": "改分类",
"IgnoreDownload": "忽略下载",
"IgnoreDownloads": "忽略下载",
"IgnoreDownloadsHint": "阻止 {appName} 进一步处理这些下载",
@@ -1808,5 +1808,6 @@
"ChangeCategoryHint": "将下载从下载客户端更改为“导入后类别”",
"IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载",
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了某些索引器在使用RSS或者搜索期间可能无法正确拒绝它启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。",
"RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。"
"RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。",
"AutoTaggingSpecificationOriginalLanguage": "语言"
}

View File

@@ -23,7 +23,7 @@ namespace NzbDrone.Core.Messaging.Commands
}
public virtual bool UpdateScheduledTask => true;
public virtual string CompletionMessage => "Completed";
public virtual string CompletionMessage => null;
public virtual bool RequiresDiskAccess => false;
public virtual bool IsExclusive => false;
public virtual bool IsLongRunning => false;

View File

@@ -99,7 +99,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
var clientIdentifier = _configService.PlexClientIdentifier;
var requestBuilder = new HttpRequestBuilder("https://metadata.provider.plex.tv/library/sections/watchlist/all")
var requestBuilder = new HttpRequestBuilder("https://discover.provider.plex.tv/library/sections/watchlist/all")
.Accept(HttpAccept.Json)
.AddQueryParam("clientID", clientIdentifier)
.AddQueryParam("context[device][product]", BuildInfo.AppName)
@@ -107,7 +107,8 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
.AddQueryParam("context[device][platformVersion]", "7")
.AddQueryParam("context[device][version]", BuildInfo.Version.ToString())
.AddQueryParam("includeFields", "title,type,year,ratingKey")
.AddQueryParam("includeElements", "Guid")
.AddQueryParam("excludeElements", "Image")
.AddQueryParam("includeGuids", "1")
.AddQueryParam("sort", "watchlistedAt:desc")
.AddQueryParam("type", (int)PlexMediaType.Show)
.AddQueryParam("X-Plex-Container-Size", pageSize)

View File

@@ -47,13 +47,13 @@ namespace NzbDrone.Core.Organizer
private readonly ICached<bool> _patternHasEpisodeIdentifierCache;
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);
private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
public static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
public static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})",
@@ -698,7 +698,7 @@ namespace NzbDrone.Core.Organizer
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 =>
{
if (m.CustomFormat.IsNullOrWhiteSpace())
@@ -717,6 +717,29 @@ namespace NzbDrone.Core.Organizer
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)
{
var tokens = new List<string>();

View File

@@ -75,6 +75,7 @@ namespace NzbDrone.Core.Organizer
}
return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) ||
(FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) ||
FileNameValidation.OriginalTokenRegex.IsMatch(value);
}
}
@@ -91,6 +92,7 @@ namespace NzbDrone.Core.Organizer
}
return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) ||
(FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) ||
FileNameBuilder.AirDateRegex.IsMatch(value) ||
FileNameValidation.OriginalTokenRegex.IsMatch(value);
}
@@ -109,6 +111,7 @@ namespace NzbDrone.Core.Organizer
}
return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) ||
(FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) ||
FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) ||
FileNameValidation.OriginalTokenRegex.IsMatch(value);
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FluentValidation.Results;
using NzbDrone.Core.Parser.Model;
@@ -20,7 +21,9 @@ namespace NzbDrone.Core.Organizer
public ValidationFailure ValidateStandardFilename(SampleResult sampleResult)
{
var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE);
var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName);
var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar)
? Parser.Parser.ParsePath(sampleResult.FileName)
: Parser.Parser.ParseTitle(sampleResult.FileName);
if (parsedEpisodeInfo == null)
{
@@ -38,7 +41,9 @@ namespace NzbDrone.Core.Organizer
public ValidationFailure ValidateDailyFilename(SampleResult sampleResult)
{
var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE);
var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName);
var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar)
? Parser.Parser.ParsePath(sampleResult.FileName)
: Parser.Parser.ParseTitle(sampleResult.FileName);
if (parsedEpisodeInfo == null)
{
@@ -66,7 +71,9 @@ namespace NzbDrone.Core.Organizer
public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult)
{
var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE);
var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName);
var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar)
? Parser.Parser.ParsePath(sampleResult.FileName)
: Parser.Parser.ParseTitle(sampleResult.FileName);
if (parsedEpisodeInfo == null)
{

View File

@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// 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),
// Anime - [SubGroup] Title Absolute Episode Number + Season+Episode
@@ -186,6 +186,10 @@ namespace NzbDrone.Core.Parser
new Regex(@"^((?<title>.*?)[ ._]\/[ ._])+\(?S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))(?:-(?<episode>(?<!\d+)\d{1,2}(?!\d+)))?([ ._]of[ ._]\d+)?\)?[ ._][\(\[]",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode with title (S01E99-100, S01E05-06)
new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))E(?<episode>\d{2,3}(?!\d+))(?:-(?<episode>\d{2,3}(?!\d+)))+(?:[-_. ]|$)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode with title (S01E05-06, S01E05-6)
new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))E(?<episode>\d{1,2}(?!\d+))(?:-(?<episode>\d{1,2}(?!\d+)))+(?:[-_. ]|$)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -401,6 +405,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}[)\]])?$",
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
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -546,6 +554,8 @@ namespace NzbDrone.Core.Parser
private static readonly Regex ArticleWordRegex = new Regex(@"^(a|an|the)\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
private static readonly Regex SeasonFolderRegex = new Regex(@"^(?:S|Season|Saison|Series|Stagione)[-_. ]*(?<season>(?<!\d+)\d{1,4}(?!\d+))(?:[_. ]+(?!\d+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleEpisodeNumberRegex = new Regex(@"^[ex]?(?<episode>(?<!\d+)\d{1,3}(?!\d+))(?:[ex-](?<episode>(?<!\d+)\d{1,3}(?!\d+)))?(?:[_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled);
@@ -555,6 +565,28 @@ namespace NzbDrone.Core.Parser
{
var fileInfo = new FileInfo(path);
// Parse using the folder and file separately, but combine if they both parse correctly.
var episodeNumberMatch = SimpleEpisodeNumberRegex.Matches(fileInfo.Name);
if (episodeNumberMatch.Count != 0 && fileInfo.Directory?.Name != null)
{
var parsedFileInfo = ParseMatchCollection(episodeNumberMatch, fileInfo.Name);
if (parsedFileInfo != null)
{
var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name);
if (seasonMatch.Success && seasonMatch.Groups["season"].Success)
{
parsedFileInfo.SeasonNumber = int.Parse(seasonMatch.Groups["season"].Value);
Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo);
return parsedFileInfo;
}
}
}
var result = ParseTitle(fileInfo.Name);
if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number))

View File

@@ -1,10 +1,13 @@
using System;
using System;
using System.Threading;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.ProgressMessaging
{
public static class ProgressMessageContext
{
private static AsyncLocal<CommandModel> _commandModelAsync = new AsyncLocal<CommandModel>();
[ThreadStatic]
private static CommandModel _commandModel;
@@ -13,8 +16,15 @@ namespace NzbDrone.Core.ProgressMessaging
public static CommandModel CommandModel
{
get { return _commandModel; }
set { _commandModel = value; }
get
{
return _commandModel ?? _commandModelAsync.Value;
}
set
{
_commandModel = value;
_commandModelAsync.Value = value;
}
}
public static bool LockReentrancy()

View File

@@ -7,7 +7,7 @@
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
<PackageReference Include="MailKit" Version="3.6.0" />
<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.FFprobe" Version="5.1.4.112" />
<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.Postgres" Version="3.3.2.9" />
<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="NLog" Version="4.7.14" />
<PackageReference Include="MonoTorrent" Version="2.0.7" />
<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" />
</ItemGroup>
<ItemGroup>

View File

@@ -39,5 +39,7 @@ namespace NzbDrone.Core.Tv.Commands
public override bool UpdateScheduledTask => SeriesIds.Empty();
public override bool IsLongRunning => true;
public override string CompletionMessage => "Completed";
}
}

View File

@@ -6,7 +6,5 @@ namespace NzbDrone.Core.Update.Commands
{
public override bool SendUpdatesToClient => true;
public override bool IsExclusive => true;
public override string CompletionMessage => null;
}
}

View File

@@ -8,8 +8,8 @@
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,7 +4,7 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<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="NLog" Version="4.7.14" />
</ItemGroup>

View File

@@ -41,7 +41,8 @@ namespace Sonarr.Api.V3.Indexers
_logger = logger;
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.PublishDate).NotEmpty();
}
@@ -50,7 +51,7 @@ namespace Sonarr.Api.V3.Indexers
[Consumes("application/json")]
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);