mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
11 Commits
v4.0.11.27
...
v4.0.11.27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36633b5d08 | ||
|
|
1374240321 | ||
|
|
f1d54d2a9a | ||
|
|
03b8c4c28e | ||
|
|
4e4bf3507f | ||
|
|
34ae65c087 | ||
|
|
ebe23104d4 | ||
|
|
e8c3aa20bd | ||
|
|
6c231cbe6a | ||
|
|
8ce688186e | ||
|
|
04ebf03fb5 |
@@ -70,6 +70,7 @@ interface AppState {
|
||||
captcha: CaptchaAppState;
|
||||
commands: CommandAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
episodeHistory: HistoryAppState;
|
||||
episodes: EpisodesAppState;
|
||||
episodesSelection: EpisodesAppState;
|
||||
history: HistoryAppState;
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
clearReleases,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeHistoryConnector from './History/EpisodeHistoryConnector';
|
||||
import EpisodeSearchConnector from './Search/EpisodeSearchConnector';
|
||||
import EpisodeHistory from './History/EpisodeHistory';
|
||||
import EpisodeSearch from './Search/EpisodeSearch';
|
||||
import SeasonEpisodeNumber from './SeasonEpisodeNumber';
|
||||
import EpisodeSummary from './Summary/EpisodeSummary';
|
||||
import styles from './EpisodeDetailsModalContent.css';
|
||||
@@ -168,13 +168,13 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
||||
|
||||
<TabPanel>
|
||||
<div className={styles.tabContent}>
|
||||
<EpisodeHistoryConnector episodeId={episodeId} />
|
||||
<EpisodeHistory episodeId={episodeId} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
{/* Don't wrap in tabContent so we not have a top margin */}
|
||||
<EpisodeSearchConnector
|
||||
<EpisodeSearch
|
||||
episodeId={episodeId}
|
||||
startInteractiveSearch={startInteractiveSearch}
|
||||
onModalClose={onModalClose}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeHistoryRow from './EpisodeHistoryRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'eventType',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class EpisodeHistory extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onMarkAsFailedPress
|
||||
} = this.props;
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<Alert kind={kinds.DANGER}>{translate('EpisodeHistoryLoadError')}</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPopulated && !hasItems && !error) {
|
||||
return (
|
||||
<Alert kind={kinds.INFO}>{translate('NoEpisodeHistory')}</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPopulated && hasItems && !error) {
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<EpisodeHistoryRow
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeHistory.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
EpisodeHistory.defaultProps = {
|
||||
selectedTab: 'details'
|
||||
};
|
||||
|
||||
export default EpisodeHistory;
|
||||
129
frontend/src/Episode/History/EpisodeHistory.tsx
Normal file
129
frontend/src/Episode/History/EpisodeHistory.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
clearEpisodeHistory,
|
||||
episodeHistoryMarkAsFailed,
|
||||
fetchEpisodeHistory,
|
||||
} from 'Store/Actions/episodeHistoryActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeHistoryRow from './EpisodeHistoryRow';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
interface EpisodeHistoryProps {
|
||||
episodeId: number;
|
||||
}
|
||||
|
||||
function EpisodeHistory({ episodeId }: EpisodeHistoryProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { items, isFetching, isPopulated, error } = useSelector(
|
||||
(state: AppState) => state.episodeHistory
|
||||
);
|
||||
|
||||
const handleMarkAsFailedPress = useCallback(
|
||||
(historyId: number) => {
|
||||
dispatch(episodeHistoryMarkAsFailed({ historyId, episodeId }));
|
||||
},
|
||||
[episodeId, dispatch]
|
||||
);
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchEpisodeHistory({ episodeId }));
|
||||
|
||||
return () => {
|
||||
dispatch(clearEpisodeHistory());
|
||||
};
|
||||
}, [episodeId, dispatch]);
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<Alert kind={kinds.DANGER}>{translate('EpisodeHistoryLoadError')}</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPopulated && !hasItems && !error) {
|
||||
return <Alert kind={kinds.INFO}>{translate('NoEpisodeHistory')}</Alert>;
|
||||
}
|
||||
|
||||
if (isPopulated && hasItems && !error) {
|
||||
return (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<EpisodeHistoryRow
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default EpisodeHistory;
|
||||
@@ -1,63 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearEpisodeHistory, episodeHistoryMarkAsFailed, fetchEpisodeHistory } from 'Store/Actions/episodeHistoryActions';
|
||||
import EpisodeHistory from './EpisodeHistory';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.episodeHistory,
|
||||
(episodeHistory) => {
|
||||
return episodeHistory;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchEpisodeHistory,
|
||||
clearEpisodeHistory,
|
||||
episodeHistoryMarkAsFailed
|
||||
};
|
||||
|
||||
class EpisodeHistoryConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchEpisodeHistory({ episodeId: this.props.episodeId });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearEpisodeHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = (historyId) => {
|
||||
this.props.episodeHistoryMarkAsFailed({ historyId, episodeId: this.props.episodeId });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EpisodeHistory
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeHistoryConnector.propTypes = {
|
||||
episodeId: PropTypes.number.isRequired,
|
||||
fetchEpisodeHistory: PropTypes.func.isRequired,
|
||||
clearEpisodeHistory: PropTypes.func.isRequired,
|
||||
episodeHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeHistoryConnector);
|
||||
@@ -1,177 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HistoryDetails from 'Activity/History/Details/HistoryDetails';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EpisodeHistoryRow.css';
|
||||
|
||||
function getTitle(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed': return 'Grabbed';
|
||||
case 'seriesFolderImported': return 'Series Folder Imported';
|
||||
case 'downloadFolderImported': return 'Download Folder Imported';
|
||||
case 'downloadFailed': return 'Download Failed';
|
||||
case 'episodeFileDeleted': return 'Episode File Deleted';
|
||||
case 'episodeFileRenamed': return 'Episode File Renamed';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
class EpisodeHistoryRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isMarkAsFailedModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: true });
|
||||
};
|
||||
|
||||
onConfirmMarkAsFailed = () => {
|
||||
this.props.onMarkAsFailedPress(this.props.id);
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
};
|
||||
|
||||
onMarkAsFailedModalClose = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
languages,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
date,
|
||||
data,
|
||||
downloadId
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isMarkAsFailedModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<HistoryEventTypeCell
|
||||
eventType={eventType}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCell
|
||||
date={date}
|
||||
includeSeconds={true}
|
||||
includeTime={true}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={getTitle(eventType)}
|
||||
body={
|
||||
<HistoryDetails
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMarkAsFailedModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('MarkAsFailed')}
|
||||
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
|
||||
confirmLabel={translate('MarkAsFailed')}
|
||||
onConfirm={this.onConfirmMarkAsFailed}
|
||||
onCancel={this.onMarkAsFailedModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeHistoryRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeHistoryRow;
|
||||
151
frontend/src/Episode/History/EpisodeHistoryRow.tsx
Normal file
151
frontend/src/Episode/History/EpisodeHistoryRow.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import HistoryDetails from 'Activity/History/Details/HistoryDetails';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EpisodeHistoryRow.css';
|
||||
|
||||
function getTitle(eventType: HistoryEventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return 'Grabbed';
|
||||
case 'seriesFolderImported':
|
||||
return 'Series Folder Imported';
|
||||
case 'downloadFolderImported':
|
||||
return 'Download Folder Imported';
|
||||
case 'downloadFailed':
|
||||
return 'Download Failed';
|
||||
case 'episodeFileDeleted':
|
||||
return 'Episode File Deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode File Renamed';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
interface EpisodeHistoryRowProps {
|
||||
id: number;
|
||||
eventType: HistoryEventType;
|
||||
sourceTitle: string;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
qualityCutoffNotMet: boolean;
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
date: string;
|
||||
data: HistoryData;
|
||||
downloadId?: string;
|
||||
onMarkAsFailedPress: (id: number) => void;
|
||||
}
|
||||
|
||||
function EpisodeHistoryRow({
|
||||
id,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
languages,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
date,
|
||||
data,
|
||||
downloadId,
|
||||
onMarkAsFailedPress,
|
||||
}: EpisodeHistoryRowProps) {
|
||||
const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false);
|
||||
|
||||
const handleMarkAsFailedPress = useCallback(() => {
|
||||
setIsMarkAsFailedModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmMarkAsFailed = useCallback(() => {
|
||||
onMarkAsFailedPress(id);
|
||||
setIsMarkAsFailedModalOpen(false);
|
||||
}, [id, onMarkAsFailedPress]);
|
||||
|
||||
const handleMarkAsFailedModalClose = useCallback(() => {
|
||||
setIsMarkAsFailedModalOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<HistoryEventTypeCell eventType={eventType} data={data} />
|
||||
|
||||
<TableRowCell>{sourceTitle}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCell date={date} includeSeconds={true} includeTime={true} />
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<Popover
|
||||
anchor={<Icon name={icons.INFO} />}
|
||||
title={getTitle(eventType)}
|
||||
body={
|
||||
<HistoryDetails
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
|
||||
{eventType === 'grabbed' && (
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={handleMarkAsFailedPress}
|
||||
/>
|
||||
)}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMarkAsFailedModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('MarkAsFailed')}
|
||||
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
|
||||
confirmLabel={translate('MarkAsFailed')}
|
||||
onConfirm={handleConfirmMarkAsFailed}
|
||||
onCancel={handleMarkAsFailedModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeHistoryRow;
|
||||
@@ -1,56 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EpisodeSearch.css';
|
||||
|
||||
function EpisodeSearch(props) {
|
||||
const {
|
||||
onQuickSearchPress,
|
||||
onInteractiveSearchPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
size={sizes.LARGE}
|
||||
onPress={onQuickSearchPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.buttonIcon}
|
||||
name={icons.QUICK}
|
||||
/>
|
||||
|
||||
{translate('QuickSearch')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={onInteractiveSearchPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.buttonIcon}
|
||||
name={icons.INTERACTIVE}
|
||||
/>
|
||||
|
||||
{translate('InteractiveSearch')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EpisodeSearch.propTypes = {
|
||||
onQuickSearchPress: PropTypes.func.isRequired,
|
||||
onInteractiveSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeSearch;
|
||||
80
frontend/src/Episode/Search/EpisodeSearch.tsx
Normal file
80
frontend/src/Episode/Search/EpisodeSearch.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EpisodeSearch.css';
|
||||
|
||||
interface EpisodeSearchProps {
|
||||
episodeId: number;
|
||||
startInteractiveSearch: boolean;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function EpisodeSearch({
|
||||
episodeId,
|
||||
startInteractiveSearch,
|
||||
onModalClose,
|
||||
}: EpisodeSearchProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { isPopulated } = useSelector((state: AppState) => state.releases);
|
||||
|
||||
const [isInteractiveSearchOpen, setIsInteractiveSearchOpen] = useState(
|
||||
startInteractiveSearch || isPopulated
|
||||
);
|
||||
|
||||
const handleQuickSearchPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.EPISODE_SEARCH,
|
||||
episodeIds: [episodeId],
|
||||
})
|
||||
);
|
||||
|
||||
onModalClose();
|
||||
}, [episodeId, dispatch, onModalClose]);
|
||||
|
||||
const handleInteractiveSearchPress = useCallback(() => {
|
||||
setIsInteractiveSearchOpen(true);
|
||||
}, []);
|
||||
|
||||
if (isInteractiveSearchOpen) {
|
||||
return <InteractiveSearch type="episode" searchPayload={{ episodeId }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
size={sizes.LARGE}
|
||||
onPress={handleQuickSearchPress}
|
||||
>
|
||||
<Icon className={styles.buttonIcon} name={icons.QUICK} />
|
||||
|
||||
{translate('QuickSearch')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={handleInteractiveSearchPress}
|
||||
>
|
||||
<Icon className={styles.buttonIcon} name={icons.INTERACTIVE} />
|
||||
|
||||
{translate('InteractiveSearch')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeSearch;
|
||||
@@ -1,93 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import EpisodeSearch from './EpisodeSearch';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.releases,
|
||||
(releases) => {
|
||||
return {
|
||||
isPopulated: releases.isPopulated
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class EpisodeSearchConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isInteractiveSearchOpen: props.startInteractiveSearch
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.isPopulated) {
|
||||
this.setState({ isInteractiveSearchOpen: true });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQuickSearchPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.EPISODE_SEARCH,
|
||||
episodeIds: [this.props.episodeId]
|
||||
});
|
||||
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
onInteractiveSearchPress = () => {
|
||||
this.setState({ isInteractiveSearchOpen: true });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const { episodeId } = this.props;
|
||||
|
||||
if (this.state.isInteractiveSearchOpen) {
|
||||
return (
|
||||
<InteractiveSearch
|
||||
type="episode"
|
||||
searchPayload={{ episodeId }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EpisodeSearch
|
||||
{...this.props}
|
||||
onQuickSearchPress={this.onQuickSearchPress}
|
||||
onInteractiveSearchPress={this.onInteractiveSearchPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeSearchConnector.propTypes = {
|
||||
episodeId: PropTypes.number.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
startInteractiveSearch: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeSearchConnector);
|
||||
@@ -9,26 +9,27 @@ import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MediaInfo from './MediaInfo';
|
||||
import styles from './EpisodeFileRow.css';
|
||||
|
||||
interface EpisodeFileRowProps {
|
||||
path: string;
|
||||
size: number;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
qualityCutoffNotMet: boolean;
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
mediaInfo: object;
|
||||
interface EpisodeFileRowProps
|
||||
extends Pick<
|
||||
EpisodeFile,
|
||||
| 'path'
|
||||
| 'size'
|
||||
| 'languages'
|
||||
| 'quality'
|
||||
| 'customFormats'
|
||||
| 'customFormatScore'
|
||||
| 'qualityCutoffNotMet'
|
||||
| 'mediaInfo'
|
||||
> {
|
||||
columns: Column[];
|
||||
onDeleteEpisodeFile(): void;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
|
||||
function MediaInfo(props) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
{
|
||||
Object.keys(props).map((key) => {
|
||||
const title = key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase());
|
||||
|
||||
const value = props[key];
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionListItem
|
||||
key={key}
|
||||
title={title}
|
||||
data={props[key]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaInfo;
|
||||
27
frontend/src/Episode/Summary/MediaInfo.tsx
Normal file
27
frontend/src/Episode/Summary/MediaInfo.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import MediaInfoProps from 'typings/MediaInfo';
|
||||
import getEntries from 'Utilities/Object/getEntries';
|
||||
|
||||
function MediaInfo(props: MediaInfoProps) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
{getEntries(props).map(([key, value]) => {
|
||||
const title = key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase());
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionListItem key={key} title={title} data={props[key]} />
|
||||
);
|
||||
})}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaInfo;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createEpisodeFileSelector(),
|
||||
(episodeFile) => {
|
||||
return {
|
||||
languages: episodeFile ? episodeFile.languages : undefined
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(EpisodeLanguages);
|
||||
15
frontend/src/EpisodeFile/EpisodeFileLanguages.tsx
Normal file
15
frontend/src/EpisodeFile/EpisodeFileLanguages.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import useEpisodeFile from './useEpisodeFile';
|
||||
|
||||
interface EpisodeFileLanguagesProps {
|
||||
episodeFileId: number;
|
||||
}
|
||||
|
||||
function EpisodeFileLanguages({ episodeFileId }: EpisodeFileLanguagesProps) {
|
||||
const episodeFile = useEpisodeFile(episodeFileId);
|
||||
|
||||
return <EpisodeLanguages languages={episodeFile?.languages ?? []} />;
|
||||
}
|
||||
|
||||
export default EpisodeFileLanguages;
|
||||
@@ -1,105 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import * as mediaInfoTypes from './mediaInfoTypes';
|
||||
|
||||
function formatLanguages(languages) {
|
||||
if (!languages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const splitLanguages = _.uniq(languages.split('/')).map((l) => {
|
||||
const simpleLanguage = l.split('_')[0];
|
||||
|
||||
if (simpleLanguage === 'und') {
|
||||
return translate('Unknown');
|
||||
}
|
||||
|
||||
return getLanguageName(simpleLanguage);
|
||||
}
|
||||
);
|
||||
|
||||
if (splitLanguages.length > 3) {
|
||||
return (
|
||||
<span title={splitLanguages.join(', ')}>
|
||||
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{splitLanguages.join(', ')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaInfo(props) {
|
||||
const {
|
||||
type,
|
||||
audioChannels,
|
||||
audioCodec,
|
||||
audioLanguages,
|
||||
subtitles,
|
||||
videoCodec,
|
||||
videoDynamicRangeType
|
||||
} = props;
|
||||
|
||||
if (type === mediaInfoTypes.AUDIO) {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
audioCodec ? audioCodec : ''
|
||||
}
|
||||
|
||||
{
|
||||
audioCodec && audioChannels ? ' - ' : ''
|
||||
}
|
||||
|
||||
{
|
||||
audioChannels ? audioChannels.toFixed(1) : ''
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
|
||||
return formatLanguages(audioLanguages);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.SUBTITLES) {
|
||||
return formatLanguages(subtitles);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.VIDEO) {
|
||||
return (
|
||||
<span>
|
||||
{videoCodec}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
||||
return (
|
||||
<span>
|
||||
{videoDynamicRangeType}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
MediaInfo.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
audioChannels: PropTypes.number,
|
||||
audioCodec: PropTypes.string,
|
||||
audioLanguages: PropTypes.string,
|
||||
subtitles: PropTypes.string,
|
||||
videoCodec: PropTypes.string,
|
||||
videoDynamicRangeType: PropTypes.string
|
||||
};
|
||||
|
||||
export default MediaInfo;
|
||||
92
frontend/src/EpisodeFile/MediaInfo.tsx
Normal file
92
frontend/src/EpisodeFile/MediaInfo.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import useEpisodeFile from './useEpisodeFile';
|
||||
|
||||
function formatLanguages(languages: string | undefined) {
|
||||
if (!languages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const splitLanguages = [...new Set(languages.split('/'))].map((l) => {
|
||||
const simpleLanguage = l.split('_')[0];
|
||||
|
||||
if (simpleLanguage === 'und') {
|
||||
return translate('Unknown');
|
||||
}
|
||||
|
||||
return getLanguageName(simpleLanguage);
|
||||
});
|
||||
|
||||
if (splitLanguages.length > 3) {
|
||||
return (
|
||||
<span title={splitLanguages.join(', ')}>
|
||||
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2}{' '}
|
||||
more
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{splitLanguages.join(', ')}</span>;
|
||||
}
|
||||
|
||||
export type MediaInfoType =
|
||||
| 'audio'
|
||||
| 'audioLanguages'
|
||||
| 'subtitles'
|
||||
| 'video'
|
||||
| 'videoDynamicRangeType';
|
||||
|
||||
interface MediaInfoProps {
|
||||
episodeFileId?: number;
|
||||
type: MediaInfoType;
|
||||
}
|
||||
|
||||
function MediaInfo({ episodeFileId, type }: MediaInfoProps) {
|
||||
const episodeFile = useEpisodeFile(episodeFileId);
|
||||
|
||||
if (!episodeFile?.mediaInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
audioChannels,
|
||||
audioCodec,
|
||||
audioLanguages,
|
||||
subtitles,
|
||||
videoCodec,
|
||||
videoDynamicRangeType,
|
||||
} = episodeFile.mediaInfo;
|
||||
|
||||
if (type === 'audio') {
|
||||
return (
|
||||
<span>
|
||||
{audioCodec ? audioCodec : ''}
|
||||
|
||||
{audioCodec && audioChannels ? ' - ' : ''}
|
||||
|
||||
{audioChannels ? audioChannels.toFixed(1) : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'audioLanguages') {
|
||||
return formatLanguages(audioLanguages);
|
||||
}
|
||||
|
||||
if (type === 'subtitles') {
|
||||
return formatLanguages(subtitles);
|
||||
}
|
||||
|
||||
if (type === 'video') {
|
||||
return <span>{videoCodec}</span>;
|
||||
}
|
||||
|
||||
if (type === 'videoDynamicRangeType') {
|
||||
return <span>{videoDynamicRangeType}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default MediaInfo;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
||||
import MediaInfo from './MediaInfo';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createEpisodeFileSelector(),
|
||||
(episodeFile) => {
|
||||
if (episodeFile) {
|
||||
return {
|
||||
...episodeFile.mediaInfo
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(MediaInfo);
|
||||
@@ -74,9 +74,6 @@ interface SelectEpisodeModalContentProps {
|
||||
onModalClose(): unknown;
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
||||
const {
|
||||
selectedIds,
|
||||
|
||||
@@ -13,8 +13,8 @@ import EpisodeSearchCell from 'Episode/EpisodeSearchCell';
|
||||
import EpisodeStatus from 'Episode/EpisodeStatus';
|
||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import IndexerFlags from 'Episode/IndexerFlags';
|
||||
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
||||
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
|
||||
import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages';
|
||||
import MediaInfo from 'EpisodeFile/MediaInfo';
|
||||
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@@ -229,7 +229,7 @@ class EpisodeRow extends Component {
|
||||
key={name}
|
||||
className={styles.languages}
|
||||
>
|
||||
<EpisodeFileLanguageConnector
|
||||
<EpisodeFileLanguages
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@@ -242,7 +242,7 @@ class EpisodeRow extends Component {
|
||||
key={name}
|
||||
className={styles.audio}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.AUDIO}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
@@ -256,7 +256,7 @@ class EpisodeRow extends Component {
|
||||
key={name}
|
||||
className={styles.audioLanguages}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
@@ -270,7 +270,7 @@ class EpisodeRow extends Component {
|
||||
key={name}
|
||||
className={styles.subtitles}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.SUBTITLES}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
@@ -284,7 +284,7 @@ class EpisodeRow extends Component {
|
||||
key={name}
|
||||
className={styles.video}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.VIDEO}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
@@ -298,7 +298,7 @@ class EpisodeRow extends Component {
|
||||
key={name}
|
||||
className={styles.videoDynamicRangeType}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,13 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import {
|
||||
icons,
|
||||
inputTypes,
|
||||
kinds,
|
||||
sizes,
|
||||
tooltipPositions,
|
||||
} from 'Helpers/Props';
|
||||
import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import { saveSeries, setSeriesValue } from 'Store/Actions/seriesActions';
|
||||
@@ -151,7 +157,7 @@ function EditSeriesModalContent({
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherSettings}>
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -163,7 +169,7 @@ function EditSeriesModalContent({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewSeasons')}
|
||||
<Popover
|
||||
@@ -183,7 +189,7 @@ function EditSeriesModalContent({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('UseSeasonFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -195,7 +201,7 @@ function EditSeriesModalContent({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -206,7 +212,7 @@ function EditSeriesModalContent({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('SeriesType')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -218,7 +224,7 @@ function EditSeriesModalContent({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Path')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -229,7 +235,7 @@ function EditSeriesModalContent({
|
||||
<FormInputButton
|
||||
key="fileBrowser"
|
||||
kind={kinds.DEFAULT}
|
||||
title="Root Folder"
|
||||
title={translate('RootFolder')}
|
||||
onPress={handleRootFolderPress}
|
||||
>
|
||||
<Icon name={icons.ROOT_FOLDER} />
|
||||
@@ -239,7 +245,7 @@ function EditSeriesModalContent({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchMetadata } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Metadatas from './Metadatas';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.metadata', sortByProp('name')),
|
||||
(metadata) => metadata
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchMetadata
|
||||
};
|
||||
|
||||
class MetadatasConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchMetadata();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Metadatas
|
||||
{...this.props}
|
||||
onConfirmDeleteMetadata={this.onConfirmDeleteMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MetadatasConnector.propTypes = {
|
||||
fetchMetadata: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
||||
9
frontend/src/Utilities/Object/getEntries.ts
Normal file
9
frontend/src/Utilities/Object/getEntries.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type Entries<T> = {
|
||||
[K in keyof T]: [K, T[K]];
|
||||
}[keyof T][];
|
||||
|
||||
function getEntries<T extends object>(obj: T): Entries<T> {
|
||||
return Object.entries(obj) as Entries<T>;
|
||||
}
|
||||
|
||||
export default getEntries;
|
||||
@@ -153,12 +153,15 @@ class CutoffUnmet extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('SearchSelected')}
|
||||
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!itemsSelected || isSearchingForCutoffUnmetEpisodes}
|
||||
onPress={this.onSearchSelectedPress}
|
||||
isDisabled={isSearchingForCutoffUnmetEpisodes}
|
||||
isSpinning={isSearchingForCutoffUnmetEpisodes}
|
||||
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllCutoffUnmetPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||
iconName={icons.MONITORED}
|
||||
@@ -167,17 +170,6 @@ class CutoffUnmet extends Component {
|
||||
onPress={this.onToggleSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isSearchingForCutoffUnmetEpisodes}
|
||||
onPress={this.onSearchAllCutoffUnmetPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
|
||||
@@ -18,9 +18,10 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.wanted.cutoffUnmet,
|
||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH),
|
||||
(cutoffUnmet, isSearchingForCutoffUnmetEpisodes) => {
|
||||
createCommandExecutingSelector(commandNames.EPISODE_SEARCH),
|
||||
(cutoffUnmet, isSearchingForAllCutoffUnmetEpisodes, isSearchingForSelectedCutoffUnmetEpisodes) => {
|
||||
return {
|
||||
isSearchingForCutoffUnmetEpisodes,
|
||||
isSearchingForCutoffUnmetEpisodes: isSearchingForAllCutoffUnmetEpisodes || isSearchingForSelectedCutoffUnmetEpisodes,
|
||||
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
|
||||
...cutoffUnmet
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import EpisodeSearchCell from 'Episode/EpisodeSearchCell';
|
||||
import EpisodeStatus from 'Episode/EpisodeStatus';
|
||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
||||
import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages';
|
||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||
import styles from './CutoffUnmetRow.css';
|
||||
|
||||
@@ -123,7 +123,7 @@ function CutoffUnmetRow(props) {
|
||||
key={name}
|
||||
className={styles.languages}
|
||||
>
|
||||
<EpisodeFileLanguageConnector
|
||||
<EpisodeFileLanguages
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
@@ -159,12 +159,15 @@ class Missing extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('SearchSelected')}
|
||||
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!itemsSelected || isSearchingForMissingEpisodes}
|
||||
onPress={this.onSearchSelectedPress}
|
||||
isSpinning={isSearchingForMissingEpisodes}
|
||||
isDisabled={isSearchingForMissingEpisodes}
|
||||
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllMissingPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||
iconName={icons.MONITORED}
|
||||
@@ -175,16 +178,6 @@ class Missing extends Component {
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isSearchingForMissingEpisodes}
|
||||
onPress={this.onSearchAllMissingPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManualImport')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
|
||||
@@ -17,9 +17,10 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.wanted.missing,
|
||||
createCommandExecutingSelector(commandNames.MISSING_EPISODE_SEARCH),
|
||||
(missing, isSearchingForMissingEpisodes) => {
|
||||
createCommandExecutingSelector(commandNames.EPISODE_SEARCH),
|
||||
(missing, isSearchingForAllMissingEpisodes, isSearchingForSelectedMissingEpisodes) => {
|
||||
return {
|
||||
isSearchingForMissingEpisodes,
|
||||
isSearchingForMissingEpisodes: isSearchingForAllMissingEpisodes || isSearchingForSelectedMissingEpisodes,
|
||||
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
|
||||
...missing
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore.Converters;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Converters;
|
||||
|
||||
[TestFixture]
|
||||
public class TimeSpanConverterFixture : CoreTest<TimeSpanConverter>
|
||||
{
|
||||
private SQLiteParameter _param;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_param = new SQLiteParameter();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_string_when_saving_timespan_to_db()
|
||||
{
|
||||
var span = TimeSpan.FromMilliseconds(10);
|
||||
|
||||
Subject.SetValue(_param, span);
|
||||
_param.Value.Should().Be(span.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_timespan_when_getting_string_from_db()
|
||||
{
|
||||
var span = TimeSpan.FromMilliseconds(10);
|
||||
|
||||
Subject.Parse(span.ToString()).Should().Be(span);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_zero_timespan_for_db_null_value_when_getting_from_db()
|
||||
{
|
||||
Subject.Parse(null).Should().Be(TimeSpan.Zero);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Languages;
|
||||
@@ -399,5 +400,42 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_format_cutoff_is_above_current_score_and_is_revision_upgrade()
|
||||
{
|
||||
var customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 };
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.SetupGet(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.DoNotPrefer);
|
||||
|
||||
GivenProfile(new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.SDTV.Id,
|
||||
MinFormatScore = 0,
|
||||
CutoffFormatScore = 10000,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"),
|
||||
UpgradeAllowed = false
|
||||
});
|
||||
|
||||
_parseResultSingle.Series.QualityProfile.Value.FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Format = customFormat,
|
||||
Score = 50
|
||||
}
|
||||
};
|
||||
|
||||
GivenFileQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)));
|
||||
GivenNewQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 2)));
|
||||
|
||||
GivenOldCustomFormats(new List<CustomFormat>());
|
||||
GivenNewCustomFormats(new List<CustomFormat> { customFormat });
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.DVD, new Revision(version: 2)),
|
||||
new List<CustomFormat>())
|
||||
.Should().Be(UpgradeableRejectReason.CustomFormatScore);
|
||||
.Should().Be(UpgradeableRejectReason.UpgradesNotAllowed);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.HDTV720p, new Revision(version: 1)),
|
||||
new List<CustomFormat>())
|
||||
.Should().Be(UpgradeableRejectReason.CustomFormatScore);
|
||||
.Should().Be(UpgradeableRejectReason.UpgradesNotAllowed);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
18
src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs
Normal file
18
src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters;
|
||||
|
||||
public class TimeSpanConverter : SqlMapper.TypeHandler<TimeSpan>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, TimeSpan value)
|
||||
{
|
||||
parameter.Value = value.ToString();
|
||||
}
|
||||
|
||||
public override TimeSpan Parse(object value)
|
||||
{
|
||||
return value is string str ? TimeSpan.Parse(str) : TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
@@ -201,6 +201,9 @@ namespace NzbDrone.Core.Datastore
|
||||
SqlMapper.RemoveTypeMap(typeof(Guid));
|
||||
SqlMapper.RemoveTypeMap(typeof(Guid?));
|
||||
SqlMapper.AddTypeHandler(new GuidConverter());
|
||||
SqlMapper.RemoveTypeMap(typeof(TimeSpan));
|
||||
SqlMapper.RemoveTypeMap(typeof(TimeSpan?));
|
||||
SqlMapper.AddTypeHandler(new TimeSpanConverter());
|
||||
SqlMapper.AddTypeHandler(new CommandConverter());
|
||||
SqlMapper.AddTypeHandler(new SystemVersionConverter());
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public enum DownloadRejectionReason
|
||||
HistoryCustomFormatCutoffMet,
|
||||
HistoryCustomFormatScore,
|
||||
HistoryCustomFormatScoreIncrement,
|
||||
HistoryUpgradesNotAllowed,
|
||||
NoMatchingTag,
|
||||
PropersDisabled,
|
||||
ProperForOldFile,
|
||||
@@ -53,7 +54,7 @@ public enum DownloadRejectionReason
|
||||
QueueCustomFormatCutoffMet,
|
||||
QueueCustomFormatScore,
|
||||
QueueCustomFormatScoreIncrement,
|
||||
QueueNoUpgrades,
|
||||
QueueUpgradesNotAllowed,
|
||||
QueuePropersDisabled,
|
||||
Raw,
|
||||
MustContainMissing,
|
||||
@@ -72,4 +73,5 @@ public enum DownloadRejectionReason
|
||||
DiskCustomFormatCutoffMet,
|
||||
DiskCustomFormatScore,
|
||||
DiskCustomFormatScoreIncrement,
|
||||
DiskUpgradesNotAllowed
|
||||
}
|
||||
|
||||
@@ -95,17 +95,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
case UpgradeableRejectReason.MinCustomFormatScore:
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueCustomFormatScoreIncrement, "Release in queue has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore);
|
||||
}
|
||||
|
||||
_logger.Debug("Checking if profiles allow upgrading. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality);
|
||||
|
||||
if (!_upgradableSpecification.IsUpgradeAllowed(subject.Series.QualityProfile,
|
||||
remoteEpisode.ParsedEpisodeInfo.Quality,
|
||||
queuedItemCustomFormats,
|
||||
subject.ParsedEpisodeInfo.Quality,
|
||||
subject.CustomFormats))
|
||||
{
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueNoUpgrades, "Another release is queued and the Quality profile does not allow upgrades");
|
||||
case UpgradeableRejectReason.UpgradesNotAllowed:
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.QueueUpgradesNotAllowed, "Release in queue and Quality Profile '{0}' does not allow upgrades", qualityProfile.Name);
|
||||
}
|
||||
|
||||
if (_upgradableSpecification.IsRevisionUpgrade(remoteEpisode.ParsedEpisodeInfo.Quality, subject.ParsedEpisodeInfo.Quality))
|
||||
|
||||
@@ -111,6 +111,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
|
||||
case UpgradeableRejectReason.MinCustomFormatScore:
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryCustomFormatScoreIncrement, "{0} grab event in history has Custom Format score within Custom Format score increment: {1}", rejectionSubject, qualityProfile.MinUpgradeFormatScore);
|
||||
|
||||
case UpgradeableRejectReason.UpgradesNotAllowed:
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.HistoryUpgradesNotAllowed, "{0} grab event in history and Quality Profile '{1}' does not allow upgrades", rejectionSubject, qualityProfile.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
return UpgradeableRejectReason.None;
|
||||
}
|
||||
|
||||
if (!qualityProfile.UpgradeAllowed)
|
||||
{
|
||||
_logger.Debug("Quality profile '{0}' does not allow upgrading. Skipping.", qualityProfile.Name);
|
||||
|
||||
return UpgradeableRejectReason.UpgradesNotAllowed;
|
||||
}
|
||||
|
||||
// Reject unless the user does not prefer propers/repacks and it's a revision downgrade.
|
||||
if (downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer &&
|
||||
qualityRevisionCompare < 0)
|
||||
@@ -86,7 +93,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
return UpgradeableRejectReason.CustomFormatScore;
|
||||
}
|
||||
|
||||
if (qualityProfile.UpgradeAllowed && currentFormatScore >= qualityProfile.CutoffFormatScore)
|
||||
if (currentFormatScore >= qualityProfile.CutoffFormatScore)
|
||||
{
|
||||
_logger.Debug("Existing item meets cut-off for custom formats, skipping. Existing: [{0}] ({1}). Cutoff score: {2}",
|
||||
currentCustomFormats.ConcatToString(),
|
||||
|
||||
@@ -81,6 +81,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
case UpgradeableRejectReason.MinCustomFormatScore:
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScoreIncrement, "Existing file on disk has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore);
|
||||
|
||||
case UpgradeableRejectReason.UpgradesNotAllowed:
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskUpgradesNotAllowed, "Existing file on disk and Quality Profile '{0}' does not allow upgrades", qualityProfile.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
QualityCutoff,
|
||||
CustomFormatScore,
|
||||
CustomFormatCutoff,
|
||||
MinCustomFormatScore
|
||||
MinCustomFormatScore,
|
||||
UpgradesNotAllowed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"Language": "Sprache",
|
||||
"CloneCondition": "Bedingung klonen",
|
||||
"DeleteCondition": "Bedingung löschen",
|
||||
"DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{name}' löschen willst?",
|
||||
"DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{0}' löschen willst?",
|
||||
"DeleteCustomFormatMessageText": "Bist du sicher, dass du das benutzerdefinierte Format '{name}' wirklich löschen willst?",
|
||||
"RemoveSelectedItemQueueMessageText": "Bist du sicher, dass du ein Eintrag aus der Warteschlange entfernen willst?",
|
||||
"RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?",
|
||||
|
||||
@@ -999,7 +999,7 @@
|
||||
"IndexerSettingsCookie": "Cookie",
|
||||
"IndexerSettingsCookieHelpText": "If your site requires a login cookie to access the rss, you'll have to retrieve it via a browser.",
|
||||
"IndexerSettingsFailDownloads": "Fail Downloads",
|
||||
"IndexerSettingsFailDownloadsHelpText": "While processing completed downloads {appName} will treat selected errors preventing importing as failed downloads.",
|
||||
"IndexerSettingsFailDownloadsHelpText": "While processing completed downloads {appName} will treat these selected filetypes as failed downloads.",
|
||||
"IndexerSettingsMinimumSeeders": "Minimum Seeders",
|
||||
"IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.",
|
||||
"IndexerSettingsMultiLanguageRelease": "Multi Languages",
|
||||
@@ -1162,9 +1162,9 @@
|
||||
"Menu": "Menu",
|
||||
"Message": "Message",
|
||||
"Metadata": "Metadata",
|
||||
"MetadataLoadError": "Unable to load Metadata",
|
||||
"MetadataKometaDeprecated": "Kometa files will no longer be created, support will be removed completely in v5",
|
||||
"MetadataKometaDeprecatedSetting": "Deprecated",
|
||||
"MetadataLoadError": "Unable to load Metadata",
|
||||
"MetadataPlexSettingsEpisodeMappings": "Episode Mappings",
|
||||
"MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file",
|
||||
"MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File",
|
||||
@@ -1437,10 +1437,10 @@
|
||||
"NotificationsSettingsUpdateMapPathsTo": "Map Paths To",
|
||||
"NotificationsSettingsUpdateMapPathsToSeriesHelpText": "{serviceName} path, used to modify series paths when {serviceName} sees library path location differently from {appName} (Requires 'Update Library')",
|
||||
"NotificationsSettingsUseSslHelpText": "Connect to {serviceName} over HTTPS instead of HTTP",
|
||||
"NotificationsSettingsWebhookHeaders": "Headers",
|
||||
"NotificationsSettingsWebhookMethod": "Method",
|
||||
"NotificationsSettingsWebhookMethodHelpText": "Which HTTP method to use submit to the Webservice",
|
||||
"NotificationsSettingsWebhookUrl": "Webhook URL",
|
||||
"NotificationsSettingsWebhookHeaders": "Headers",
|
||||
"NotificationsSignalSettingsGroupIdPhoneNumber": "Group ID / Phone Number",
|
||||
"NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Group ID / Phone Number of the receiver",
|
||||
"NotificationsSignalSettingsPasswordHelpText": "Password used to authenticate requests toward signal-api",
|
||||
@@ -1466,6 +1466,8 @@
|
||||
"NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages",
|
||||
"NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title",
|
||||
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications",
|
||||
"NotificationsTelegramSettingsIncludeInstanceName": "Include Instance Name in Title",
|
||||
"NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Optionally include Instance name in notification",
|
||||
"NotificationsTelegramSettingsMetadataLinks": "Metadata Links",
|
||||
"NotificationsTelegramSettingsMetadataLinksHelpText": "Add a links to series metadata when sending notifications",
|
||||
"NotificationsTelegramSettingsSendSilently": "Send Silently",
|
||||
@@ -2083,14 +2085,14 @@
|
||||
"UpdateFiltered": "Update Filtered",
|
||||
"UpdateMechanismHelpText": "Use {appName}'s built-in updater or a script",
|
||||
"UpdateMonitoring": "Update Monitoring",
|
||||
"UpdatePath": "Update Path",
|
||||
"UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process",
|
||||
"UpdateSelected": "Update Selected",
|
||||
"UpdateSeriesPath": "Update Series Path",
|
||||
"UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is not writable by the user '{userName}'.",
|
||||
"UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is in an App Translocation folder.",
|
||||
"UpdateUiNotWritableHealthCheckMessage": "Cannot install update because UI folder '{uiFolder}' is not writable by the user '{userName}'.",
|
||||
"UpdaterLogFiles": "Updater Log Files",
|
||||
"UpdatePath": "Update Path",
|
||||
"UpdateSeriesPath": "Update Series Path",
|
||||
"Updates": "Updates",
|
||||
"UpgradeUntil": "Upgrade Until",
|
||||
"UpgradeUntilCustomFormatScore": "Upgrade Until Custom Format Score",
|
||||
|
||||
@@ -2137,5 +2137,9 @@
|
||||
"Menu": "Menú",
|
||||
"Premiere": "Estreno",
|
||||
"UpdateSeriesPath": "Actualizar Ruta de Series",
|
||||
"UpdatePath": "Actualizar Ruta"
|
||||
"UpdatePath": "Actualizar Ruta",
|
||||
"MetadataKometaDeprecatedSetting": "Obsoleto",
|
||||
"MetadataKometaDeprecated": "Los archivos de Kometa no seguirán siendo creados, se eliminará completamente el soporte en la v5",
|
||||
"IndexerSettingsFailDownloadsHelpText": "Mientras se procesan las descargas completadas, {appName} tratará los errores seleccionados evitando la importación como descargas fallidas.",
|
||||
"IndexerSettingsFailDownloads": "Fallo de Descargas"
|
||||
}
|
||||
|
||||
@@ -134,5 +134,8 @@
|
||||
"Search": "Cari",
|
||||
"ShowEpisodes": "Tampilkan episode",
|
||||
"Refresh": "Muat Ulang",
|
||||
"CalendarLegendEpisodeOnAirTooltip": "Episode sedang tayang"
|
||||
"CalendarLegendEpisodeOnAirTooltip": "Episode sedang tayang",
|
||||
"AddCustomFormatError": "Tidak dapat menambahkan format khusus baru, coba lagi.",
|
||||
"AddDelayProfile": "Tambah Delay Profile",
|
||||
"AddDownloadClient": "Tambahkan Download Client"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"AddToDownloadQueue": "다운로드 대기열에 추가됨",
|
||||
"NoHistory": "내역 없음",
|
||||
"SelectAll": "모두 선택",
|
||||
"View": "표시 변경",
|
||||
"View": "화면",
|
||||
"AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다",
|
||||
"AddNew": "새로 추가하기",
|
||||
"History": "내역",
|
||||
@@ -26,5 +26,34 @@
|
||||
"AddingTag": "태그 추가",
|
||||
"Analytics": "분석",
|
||||
"Age": "연령",
|
||||
"All": "모두"
|
||||
"All": "모두",
|
||||
"UsenetBlackholeNzbFolder": "Nzb 폴더",
|
||||
"UrlBase": "URL 기반",
|
||||
"TypeOfList": "{typeOfList} 목록",
|
||||
"UpdateMechanismHelpText": "{appName}의 내장 업데이트 도구 또는 스크립트 사용",
|
||||
"TorrentDelayHelpText": "토렌트를 잡기 전에 대기까지 소요되는 지연 (분)",
|
||||
"TorrentDelay": "토렌트 지연",
|
||||
"Torrents": "토렌트",
|
||||
"TorrentDelayTime": "토렌트 지연: {0torrentDelay}",
|
||||
"Unavailable": "사용 불가능",
|
||||
"UnknownEventTooltip": "알 수 없는 이벤트",
|
||||
"Warning": "경고",
|
||||
"VideoDynamicRange": "동영상 다이나믹 레인지",
|
||||
"VisitTheWikiForMoreDetails": "자세한 내용은 위키를 방문하세요: ",
|
||||
"WouldYouLikeToRestoreBackup": "'{name}' 백업을 복원하시겠습니까?",
|
||||
"XmlRpcPath": "XML RPC 경로",
|
||||
"YesterdayAt": "어제 {time}",
|
||||
"AddDelayProfileError": "새 지연 프로필을 추가할 수 없습니다. 다시 시도해주세요.",
|
||||
"AddConditionError": "새 조건을 추가 할 수 없습니다. 다시 시도해주세요.",
|
||||
"AddConditionImplementation": "조건 추가 - {implementationName}",
|
||||
"AddImportList": "가져오기 목록 추가",
|
||||
"AddImportListImplementation": "가져오기 목록 추가 - {implementationName}",
|
||||
"UpdateAvailableHealthCheckMessage": "새 업데이트 사용 가능: {version}",
|
||||
"UsenetBlackhole": "유즈넷 블랙홀",
|
||||
"AddAutoTag": "자동 태그 추가",
|
||||
"AddAutoTagError": "새 자동 태그을 추가 할 수 없습니다. 다시 시도해주세요.",
|
||||
"AddCondition": "조건 추가",
|
||||
"AddIndexerError": "새 인덱서를 추가 할 수 없습니다. 다시 시도해주세요.",
|
||||
"TorrentBlackholeTorrentFolder": "토렌트 폴더",
|
||||
"UseSsl": "SSL 사용"
|
||||
}
|
||||
|
||||
@@ -2138,5 +2138,9 @@
|
||||
"Menu": "Menu",
|
||||
"NotificationsSettingsWebhookHeaders": "Cabeçalhos",
|
||||
"UpdatePath": "Caminho da Atualização",
|
||||
"UpdateSeriesPath": "Atualizar Caminho da Série"
|
||||
"UpdateSeriesPath": "Atualizar Caminho da Série",
|
||||
"MetadataKometaDeprecated": "Os arquivos Kometa não serão mais criados, o suporte será completamente removido na v5",
|
||||
"MetadataKometaDeprecatedSetting": "Deprecado",
|
||||
"IndexerSettingsFailDownloads": "Downloads com Falhas",
|
||||
"IndexerSettingsFailDownloadsHelpText": "Durante o processamento de downloads concluídos, {appName} tratará os erros selecionados, impedindo a importação, como downloads com falha."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -367,5 +367,10 @@
|
||||
"BypassDelayIfAboveCustomFormatScore": "Пропустити, якщо перевищено оцінку користувацького формату",
|
||||
"Clone": "Клонування",
|
||||
"BlocklistFilterHasNoItems": "Вибраний фільтр чорного списку не містить елементів",
|
||||
"BypassDelayIfAboveCustomFormatScoreHelpText": "Увімкнути обхід, якщо реліз має оцінку вищу за встановлений мінімальний бал користувацького формату"
|
||||
"BypassDelayIfAboveCustomFormatScoreHelpText": "Увімкнути обхід, якщо реліз має оцінку вищу за встановлений мінімальний бал користувацького формату",
|
||||
"AutoTaggingRequiredHelpText": "Ця умова {0} має збігатися, щоб користувацький формат застосовувався. В іншому випадку достатньо одного збігу {1}.",
|
||||
"CountIndexersSelected": "{count} індексер(-и) обрано",
|
||||
"CountCustomFormatsSelected": "Користувацькі формати обрано {count}",
|
||||
"BlocklistReleaseHelpText": "Блокує завантаження цього випуску {appName} через RSS або Автоматичний пошук",
|
||||
"AutoTaggingNegateHelpText": "Якщо позначено, настроюваний формат не застосовуватиметься, якщо ця умова {0} збігається."
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
@@ -8,18 +9,23 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public class Telegram : NotificationBase<TelegramSettings>
|
||||
{
|
||||
private readonly ITelegramProxy _proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public Telegram(ITelegramProxy proxy)
|
||||
public Telegram(ITelegramProxy proxy, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public override string Name => "Telegram";
|
||||
public override string Link => "https://telegram.org/";
|
||||
|
||||
private string InstanceName => _configFileProvider.InstanceName;
|
||||
|
||||
public override void OnGrab(GrabMessage grabMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
var links = GetLinks(grabMessage.Series);
|
||||
|
||||
_proxy.SendNotification(title, grabMessage.Message, links, Settings);
|
||||
@@ -28,6 +34,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnDownload(DownloadMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
var links = GetLinks(message.Series);
|
||||
|
||||
_proxy.SendNotification(title, message.Message, links, Settings);
|
||||
@@ -36,6 +43,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnImportComplete(ImportCompleteMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
var links = GetLinks(message.Series);
|
||||
|
||||
_proxy.SendNotification(title, message.Message, links, Settings);
|
||||
@@ -44,6 +52,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
var links = GetLinks(deleteMessage.Series);
|
||||
|
||||
_proxy.SendNotification(title, deleteMessage.Message, links, Settings);
|
||||
@@ -52,6 +61,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnSeriesAdd(SeriesAddMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
var links = GetLinks(message.Series);
|
||||
|
||||
_proxy.SendNotification(title, message.Message, links, Settings);
|
||||
@@ -60,6 +70,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
var links = GetLinks(deleteMessage.Series);
|
||||
|
||||
_proxy.SendNotification(title, deleteMessage.Message, links, Settings);
|
||||
@@ -68,6 +79,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
|
||||
_proxy.SendNotification(title, healthCheck.Message, new List<TelegramLink>(), Settings);
|
||||
}
|
||||
@@ -75,6 +87,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
|
||||
_proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", new List<TelegramLink>(), Settings);
|
||||
}
|
||||
@@ -82,6 +95,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
|
||||
_proxy.SendNotification(title, updateMessage.Message, new List<TelegramLink>(), Settings);
|
||||
}
|
||||
@@ -89,6 +103,7 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message)
|
||||
{
|
||||
var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE;
|
||||
title = Settings.IncludeInstanceNameInTitle ? $"{title} - {InstanceName}" : title;
|
||||
var links = GetLinks(message.Series);
|
||||
|
||||
_proxy.SendNotification(title, message.Message, links, Settings);
|
||||
|
||||
@@ -8,6 +8,7 @@ using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Localization;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Telegram
|
||||
@@ -23,12 +24,14 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
private const string URL = "https://api.telegram.org";
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public TelegramProxy(IHttpClient httpClient, ILocalizationService localizationService, Logger logger)
|
||||
public TelegramProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, ILocalizationService localizationService, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_configFileProvider = configFileProvider;
|
||||
_localizationService = localizationService;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -70,7 +73,10 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
new TelegramLink("Sonarr.tv", "https://sonarr.tv")
|
||||
};
|
||||
|
||||
SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, links, settings);
|
||||
var testMessageTitle = settings.IncludeAppNameInTitle ? brandedTitle : title;
|
||||
testMessageTitle = settings.IncludeInstanceNameInTitle ? $"{testMessageTitle} - {_configFileProvider.InstanceName}" : testMessageTitle;
|
||||
|
||||
SendNotification(testMessageTitle, body, links, settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -51,7 +51,10 @@ namespace NzbDrone.Core.Notifications.Telegram
|
||||
[FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")]
|
||||
public bool IncludeAppNameInTitle { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")]
|
||||
[FieldDefinition(5, Label = "NotificationsTelegramSettingsIncludeInstanceName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeInstanceNameHelpText", Advanced = true)]
|
||||
public bool IncludeInstanceNameInTitle { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")]
|
||||
public IEnumerable<int> MetadataLinks { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
Reference in New Issue
Block a user