Convert History to TypeScript

(cherry picked from commit 824ed0a36931ce7aae9aa544a7baf0738dae568c)

Closes #10230
Closes #10390
Closes #10247
pull/10574/head
Mark McDowall 9 months ago committed by Bogdan
parent 13f10906f1
commit 6747b74271

@ -1,354 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import Link from 'Components/Link/Link';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
function HistoryDetails(props) {
const {
eventType,
sourceTitle,
data,
downloadId,
shortDateFormat,
timeFormat
} = props;
if (eventType === 'grabbed') {
const {
indexer,
releaseGroup,
movieMatchType,
customFormatScore,
nzbInfoUrl,
downloadClient,
downloadClientName,
age,
ageHours,
ageMinutes,
publishedDate
} = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/> :
null
}
{
releaseGroup ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseGroup')}
data={releaseGroup}
/> :
null
}
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
{
movieMatchType ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieMatchType')}
data={movieMatchType}
/> :
null
}
{
nzbInfoUrl ?
<span>
<DescriptionListItemTitle>
{translate('InfoUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span> :
null
}
{
downloadClientNameInfo ?
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClientNameInfo}
/> :
null
}
{
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
indexer ?
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
data={formatAge(age, ageHours, ageMinutes)}
/> :
null
}
{
publishedDate ?
<DescriptionListItem
title={translate('PublishedDate')}
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'downloadFailed') {
const {
message
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
message ?
<DescriptionListItem
title={translate('Message')}
data={message}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'downloadFolderImported') {
const {
customFormatScore,
droppedPath,
importedPath
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
droppedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Source')}
data={droppedPath}
/> :
null
}
{
importedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ImportedTo')}
data={importedPath}
/> :
null
}
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'movieFileDeleted') {
const {
reason,
customFormatScore
} = data;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('DeletedReasonMissingFromDisk');
break;
case 'Upgrade':
reasonMessage = translate('DeletedReasonUpgrade');
break;
default:
reasonMessage = '';
}
return (
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Reason')}
data={reasonMessage}
/>
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'movieFileRenamed') {
const {
sourcePath,
sourceRelativePath,
path,
relativePath
} = data;
return (
<DescriptionList>
<DescriptionListItem
title={translate('SourcePath')}
data={sourcePath}
/>
<DescriptionListItem
title={translate('SourceRelativePath')}
data={sourceRelativePath}
/>
<DescriptionListItem
title={translate('DestinationPath')}
data={path}
/>
<DescriptionListItem
title={translate('DestinationRelativePath')}
data={relativePath}
/>
</DescriptionList>
);
}
if (eventType === 'downloadIgnored') {
const {
message
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
message ?
<DescriptionListItem
title={translate('Message')}
data={message}
/> :
null
}
</DescriptionList>
);
}
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
</DescriptionList>
);
}
HistoryDetails.propTypes = {
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
export default HistoryDetails;

@ -0,0 +1,287 @@
import React from 'react';
import { useSelector } from 'react-redux';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import Link from 'Components/Link/Link';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import {
DownloadFailedHistory,
DownloadFolderImportedHistory,
DownloadIgnoredHistory,
GrabbedHistoryData,
HistoryData,
HistoryEventType,
MovieFileDeletedHistory,
MovieFileRenamedHistory,
} from 'typings/History';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
interface HistoryDetailsProps {
eventType: HistoryEventType;
sourceTitle: string;
data: HistoryData;
downloadId?: string;
}
function HistoryDetails(props: HistoryDetailsProps) {
const { eventType, sourceTitle, data, downloadId } = props;
const { shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
if (eventType === 'grabbed') {
const {
indexer,
releaseGroup,
movieMatchType,
customFormatScore,
nzbInfoUrl,
downloadClient,
downloadClientName,
age,
ageHours,
ageMinutes,
publishedDate,
} = data as GrabbedHistoryData;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{indexer ? (
<DescriptionListItem title={translate('Indexer')} data={indexer} />
) : null}
{releaseGroup ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseGroup')}
data={releaseGroup}
/>
) : null}
{customFormatScore && customFormatScore !== '0' ? (
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(parseInt(customFormatScore))}
/>
) : null}
{movieMatchType ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieMatchType')}
data={movieMatchType}
/>
) : null}
{nzbInfoUrl ? (
<span>
<DescriptionListItemTitle>
{translate('InfoUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span>
) : null}
{downloadClientNameInfo ? (
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClientNameInfo}
/>
) : null}
{downloadId ? (
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
) : null}
{age || ageHours || ageMinutes ? (
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
data={formatAge(age, ageHours, ageMinutes)}
/>
) : null}
{publishedDate ? (
<DescriptionListItem
title={translate('PublishedDate')}
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, {
includeSeconds: true,
})}
/>
) : null}
</DescriptionList>
);
}
if (eventType === 'downloadFailed') {
const { message } = data as DownloadFailedHistory;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{downloadId ? (
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
) : null}
{message ? (
<DescriptionListItem title={translate('Message')} data={message} />
) : null}
</DescriptionList>
);
}
if (eventType === 'downloadFolderImported') {
const { customFormatScore, droppedPath, importedPath } =
data as DownloadFolderImportedHistory;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{droppedPath ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Source')}
data={droppedPath}
/>
) : null}
{importedPath ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ImportedTo')}
data={importedPath}
/>
) : null}
{customFormatScore && customFormatScore !== '0' ? (
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(parseInt(customFormatScore))}
/>
) : null}
</DescriptionList>
);
}
if (eventType === 'movieFileDeleted') {
const { reason, customFormatScore } = data as MovieFileDeletedHistory;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('DeletedReasonMovieMissingFromDisk');
break;
case 'Upgrade':
reasonMessage = translate('DeletedReasonUpgrade');
break;
default:
reasonMessage = '';
}
return (
<DescriptionList>
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
<DescriptionListItem title={translate('Reason')} data={reasonMessage} />
{customFormatScore && customFormatScore !== '0' ? (
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(parseInt(customFormatScore))}
/>
) : null}
</DescriptionList>
);
}
if (eventType === 'movieFileRenamed') {
const { sourcePath, sourceRelativePath, path, relativePath } =
data as MovieFileRenamedHistory;
return (
<DescriptionList>
<DescriptionListItem
title={translate('SourcePath')}
data={sourcePath}
/>
<DescriptionListItem
title={translate('SourceRelativePath')}
data={sourceRelativePath}
/>
<DescriptionListItem title={translate('DestinationPath')} data={path} />
<DescriptionListItem
title={translate('DestinationRelativePath')}
data={relativePath}
/>
</DescriptionList>
);
}
if (eventType === 'downloadIgnored') {
const { message } = data as DownloadIgnoredHistory;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{downloadId ? (
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
) : null}
{message ? (
<DescriptionListItem title={translate('Message')} data={message} />
) : null}
</DescriptionList>
);
}
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
</DescriptionList>
);
}
export default HistoryDetails;

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import HistoryDetails from './HistoryDetails';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return {
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
export default connect(createMapStateToProps)(HistoryDetails);

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { HistoryData, HistoryEventType } from 'typings/History';
import translate from 'Utilities/String/translate';
import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
function getHeaderTitle(eventType: HistoryEventType) {
switch (eventType) {
case 'grabbed':
return translate('Grabbed');
@ -31,29 +31,33 @@ function getHeaderTitle(eventType) {
}
}
function HistoryDetailsModal(props) {
interface HistoryDetailsModalProps {
isOpen: boolean;
eventType: HistoryEventType;
sourceTitle: string;
data: HistoryData;
downloadId?: string;
isMarkingAsFailed: boolean;
onMarkAsFailedPress: () => void;
onModalClose: () => void;
}
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
const {
isOpen,
eventType,
sourceTitle,
data,
downloadId,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
isMarkingAsFailed = false,
onMarkAsFailedPress,
onModalClose
onModalClose,
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{getHeaderTitle(eventType)}
</ModalHeader>
<ModalHeader>{getHeaderTitle(eventType)}</ModalHeader>
<ModalBody>
<HistoryDetails
@ -61,50 +65,26 @@ function HistoryDetailsModal(props) {
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
</ModalBody>
<ModalFooter>
{
eventType === 'grabbed' &&
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
{translate('MarkAsFailed')}
</SpinnerButton>
}
{eventType === 'grabbed' && (
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
{translate('MarkAsFailed')}
</SpinnerButton>
)}
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
HistoryDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
HistoryDetailsModal.defaultProps = {
isMarkingAsFailed: false
};
export default HistoryDetailsModal;

@ -1,158 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
isMoviesFetching,
isMoviesPopulated,
moviesError,
items,
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
onFilterSelect,
onFirstPagePress,
...otherProps
} = this.props;
const isFetchingAny = isFetching || isMoviesFetching;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
const hasError = error || moviesError;
return (
<PageContent title={translate('History')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isFetching}
onPress={onFirstPagePress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetchingAny && !isAllPopulated &&
<LoadingIndicator />
}
{
!isFetchingAny && hasError &&
<Alert kind={kinds.DANGER}>
{translate('HistoryLoadError')}
</Alert>
}
{
// If history isPopulated and it's empty show no history found and don't
// wait for the episodes to populate because they are never coming.
isPopulated && !hasError && !items.length &&
<Alert kind={kinds.INFO}>
{translate('NoHistoryFound')}
</Alert>
}
{
isAllPopulated && !hasError && !!items.length &&
<div>
<Table
columns={columns}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return (
<HistoryRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={onFirstPagePress}
{...otherProps}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
History.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired
};
export default History;

@ -0,0 +1,216 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import { align, icons, kinds } from 'Helpers/Props';
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
import {
clearHistory,
fetchHistory,
gotoHistoryPage,
setHistoryFilter,
setHistorySort,
setHistoryTableOption,
} from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { TableOptionsChangePayload } from 'typings/Table';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRow from './HistoryRow';
function History() {
const requestCurrentPage = useCurrentPage();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
} = useSelector((state: AppState) => state.history);
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
createMoviesFetchingSelector()
);
const customFilters = useSelector(createCustomFiltersSelector('history'));
const dispatch = useDispatch();
const isFetchingAny = isFetching || isMoviesFetching;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
const hasError = error || moviesError;
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoHistoryPage,
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
dispatch(setHistoryFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setHistorySort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setHistoryTableOption(payload));
if (payload.pageSize) {
dispatch(gotoHistoryPage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchHistory());
} else {
dispatch(gotoHistoryPage({ page: 1 }));
}
return () => {
dispatch(clearHistory());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchHistory());
};
registerPagePopulator(repopulate);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
return (
<PageContent title={translate('History')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isFetching}
onPress={handleFirstPagePress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetchingAny && !isAllPopulated ? <LoadingIndicator /> : null}
{!isFetchingAny && hasError ? (
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
) : null}
{
// If history isPopulated and it's empty show no history found and don't
// wait for the movies to populate because they are never coming.
isPopulated && !hasError && !items.length ? (
<Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert>
) : null
}
{isAllPopulated && !hasError && items.length ? (
<div>
<Table
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<HistoryRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
</div>
) : null}
</PageContentBody>
</PageContent>
);
}
export default History;

@ -1,141 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.movies,
createCustomFiltersSelector('history'),
(history, movies, customFilters) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
customFilters,
...history
};
}
);
}
const mapDispatchToProps = {
...historyActions
};
class HistoryConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchHistory,
gotoHistoryFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchHistory();
} else {
gotoHistoryFirstPage();
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearHistory();
}
//
// Control
repopulate = () => {
this.props.fetchHistory();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoHistoryFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoHistoryPreviousPage();
};
onNextPagePress = () => {
this.props.gotoHistoryNextPage();
};
onLastPagePress = () => {
this.props.gotoHistoryLastPage();
};
onPageSelect = (page) => {
this.props.gotoHistoryPage({ page });
};
onSortPress = (sortKey) => {
this.props.setHistorySort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setHistoryFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setHistoryTableOption(payload);
if (payload.pageSize) {
this.props.gotoHistoryFirstPage();
}
};
//
// Render
render() {
return (
<History
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
{...this.props}
/>
);
}
}
HistoryConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchHistory: PropTypes.func.isRequired,
gotoHistoryFirstPage: PropTypes.func.isRequired,
gotoHistoryPreviousPage: PropTypes.func.isRequired,
gotoHistoryNextPage: PropTypes.func.isRequired,
gotoHistoryLastPage: PropTypes.func.isRequired,
gotoHistoryPage: PropTypes.func.isRequired,
setHistorySort: PropTypes.func.isRequired,
setHistoryFilter: PropTypes.func.isRequired,
setHistoryTableOption: PropTypes.func.isRequired,
clearHistory: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
);

@ -1,12 +1,17 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import {
GrabbedHistoryData,
HistoryData,
HistoryEventType,
MovieFileDeletedHistory,
} from 'typings/History';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
function getIconName(eventType, data) {
function getIconName(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
@ -17,7 +22,9 @@ function getIconName(eventType, data) {
case 'downloadFailed':
return icons.DOWNLOADING;
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
? icons.FILE_MISSING
: icons.DELETE;
case 'movieFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
@ -27,7 +34,7 @@ function getIconName(eventType, data) {
}
}
function getIconKind(eventType) {
function getIconKind(eventType: HistoryEventType) {
switch (eventType) {
case 'downloadFailed':
return kinds.DANGER;
@ -36,52 +43,47 @@ function getIconKind(eventType) {
}
}
function getTooltip(eventType, data) {
function getTooltip(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
return translate('MovieGrabbedTooltip', {
indexer: (data as GrabbedHistoryData).indexer,
downloadClient: (data as GrabbedHistoryData).downloadClient,
});
case 'movieFolderImported':
return translate('MovieFolderImportedTooltip');
case 'downloadFolderImported':
return translate('MovieImportedTooltip');
case 'downloadFailed':
return translate('MovieDownloadFailedTooltip');
return translate('DownloadFailedMovieTooltip');
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
? translate('MovieFileMissingTooltip')
: translate('MovieFileDeletedTooltip');
case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip');
case 'downloadIgnored':
return translate('MovieDownloadIgnoredTooltip');
return translate('DownloadIgnoredMovieTooltip');
default:
return translate('UnknownEventTooltip');
}
}
function HistoryEventTypeCell({ eventType, data }) {
interface HistoryEventTypeCellProps {
eventType: HistoryEventType;
data: HistoryData;
}
function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
const iconName = getIconName(eventType, data);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);
return (
<TableRowCell
className={styles.cell}
title={tooltip}
>
<Icon
name={iconName}
kind={iconKind}
/>
<TableRowCell className={styles.cell} title={tooltip}>
<Icon name={iconName} kind={iconKind} />
</TableRowCell>
);
}
HistoryEventTypeCell.propTypes = {
eventType: PropTypes.string.isRequired,
data: PropTypes.object
};
HistoryEventTypeCell.defaultProps = {
data: {}
};
export default HistoryEventTypeCell;

@ -1,277 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css';
class HistoryRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
componentDidUpdate(prevProps) {
if (
prevProps.isMarkingAsFailed &&
!this.props.isMarkingAsFailed &&
!this.props.markAsFailedError
) {
this.setState({ isDetailsModalOpen: false });
}
}
//
// Listeners
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
//
// Render
render() {
const {
movie,
quality,
customFormats,
customFormatScore,
languages,
qualityCutoffNotMet,
eventType,
sourceTitle,
date,
data,
downloadId,
isMarkingAsFailed,
columns,
shortDateFormat,
timeFormat,
onMarkAsFailedPress
} = this.props;
if (!movie) {
return null;
}
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'eventType') {
return (
<HistoryEventTypeCell
key={name}
eventType={eventType}
data={data}
/>
);
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/>
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
<MovieQuality
quality={quality}
isCutoffMet={qualityCutoffNotMet}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCell
key={name}
date={date}
/>
);
}
if (name === 'downloadClient') {
return (
<TableRowCell
key={name}
className={styles.downloadClient}
>
{data.downloadClient}
</TableRowCell>
);
}
if (name === 'indexer') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{data.indexer}
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell
key={name}
className={styles.releaseGroup}
>
{data.releaseGroup}
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell
key={name}
className={styles.details}
>
<div className={styles.actionContents}>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
</div>
</TableRowCell>
);
}
return null;
})
}
<HistoryDetailsModal
isOpen={this.state.isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onMarkAsFailedPress={onMarkAsFailedPress}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
HistoryRow.propTypes = {
movieId: PropTypes.number,
movie: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool,
markAsFailedError: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
HistoryRow.defaultProps = {
customFormats: []
};
export default HistoryRow;

@ -0,0 +1,224 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import useMovie from 'Movie/useMovie';
import { QualityModel } from 'Quality/Quality';
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
import CustomFormat from 'typings/CustomFormat';
import { HistoryData, HistoryEventType } from 'typings/History';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css';
interface HistoryRowProps {
id: number;
movieId: number;
languages: Language[];
quality: QualityModel;
customFormats?: CustomFormat[];
customFormatScore: number;
qualityCutoffNotMet: boolean;
eventType: HistoryEventType;
sourceTitle: string;
date: string;
data: HistoryData;
downloadId?: string;
isMarkingAsFailed?: boolean;
markAsFailedError?: object;
columns: Column[];
}
function HistoryRow(props: HistoryRowProps) {
const {
id,
movieId,
languages,
quality,
customFormats = [],
customFormatScore,
qualityCutoffNotMet,
eventType,
sourceTitle,
date,
data,
downloadId,
isMarkingAsFailed = false,
markAsFailedError,
columns,
} = props;
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
const dispatch = useDispatch();
const movie = useMovie(movieId);
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const handleDetailsPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const handleDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
const handleMarkAsFailedPress = useCallback(() => {
dispatch(markAsFailed({ id }));
}, [id, dispatch]);
useEffect(() => {
if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) {
setIsDetailsModalOpen(false);
dispatch(fetchHistory());
}
}, [
wasMarkingAsFailed,
isMarkingAsFailed,
markAsFailedError,
setIsDetailsModalOpen,
dispatch,
]);
if (!movie) {
return null;
}
return (
<TableRow>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'eventType') {
return (
<HistoryEventTypeCell
key={name}
eventType={eventType}
data={data}
/>
);
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages languages={languages} />
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
<MovieQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats formats={customFormats} />
</TableRowCell>
);
}
if (name === 'date') {
return <RelativeDateCell key={name} date={date} />;
}
if (name === 'downloadClient') {
return (
<TableRowCell key={name} className={styles.downloadClient}>
{'downloadClient' in data ? data.downloadClient : ''}
</TableRowCell>
);
}
if (name === 'indexer') {
return (
<TableRowCell key={name} className={styles.indexer}>
{'indexer' in data ? data.indexer : ''}
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell key={name} className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell key={name} className={styles.releaseGroup}>
{'releaseGroup' in data ? data.releaseGroup : ''}
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
}
if (name === 'details') {
return (
<TableRowCell key={name} className={styles.details}>
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
</TableRowCell>
);
}
return null;
})}
<HistoryDetailsModal
isOpen={isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
isMarkingAsFailed={isMarkingAsFailed}
onMarkAsFailedPress={handleMarkAsFailedPress}
onModalClose={handleDetailsModalClose}
/>
</TableRow>
);
}
export default HistoryRow;

@ -1,73 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import HistoryRow from './HistoryRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
return {
movie,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
const mapDispatchToProps = {
fetchHistory,
markAsFailed
};
class HistoryRowConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps) {
if (
prevProps.isMarkingAsFailed &&
!this.props.isMarkingAsFailed &&
!this.props.markAsFailedError
) {
this.props.fetchHistory();
}
}
//
// Listeners
onMarkAsFailedPress = () => {
this.props.markAsFailed({ id: this.props.id });
};
//
// Render
render() {
return (
<HistoryRow
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
);
}
}
HistoryRowConnector.propTypes = {
id: PropTypes.number.isRequired,
isMarkingAsFailed: PropTypes.bool,
markAsFailedError: PropTypes.object,
fetchHistory: PropTypes.func.isRequired,
markAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);

@ -1,7 +1,7 @@
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import Blocklist from 'Activity/Blocklist/Blocklist';
import HistoryConnector from 'Activity/History/HistoryConnector';
import History from 'Activity/History/History';
import Queue from 'Activity/Queue/Queue';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
@ -79,7 +79,7 @@ function AppRoutes() {
Activity
*/}
<Route path="/activity/history" component={HistoryConnector} />
<Route path="/activity/history" component={History} />
<Route path="/activity/queue" component={Queue} />

@ -1,10 +1,14 @@
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from 'App/State/AppSectionState';
import History from 'typings/History';
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History> {}
AppSectionFilterState<History>,
PagedAppSectionState,
TableAppSectionState {}
export default HistoryAppState;

@ -1,15 +0,0 @@
import _ from 'lodash';
function selectUniqueIds(items, idProp) {
const ids = _.reduce(items, (result, item) => {
if (item[idProp]) {
result.push(item[idProp]);
}
return result;
}, []);
return _.uniq(ids);
}
export default selectUniqueIds;

@ -0,0 +1,13 @@
import KeysMatching from 'typings/Helpers/KeysMatching';
function selectUniqueIds<T, K>(items: T[], idProp: KeysMatching<T, K>) {
return items.reduce((acc: K[], item) => {
if (item[idProp] && acc.indexOf(item[idProp] as K) === -1) {
acc.push(item[idProp] as K);
}
return acc;
}, []);
}
export default selectUniqueIds;

@ -1,4 +1,4 @@
type KeysMatching<T, V> = {
export type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];

@ -11,6 +11,61 @@ export type HistoryEventType =
| 'movieFileRenamed'
| 'downloadIgnored';
export interface GrabbedHistoryData {
indexer: string;
nzbInfoUrl: string;
releaseGroup: string;
age: string;
ageHours: string;
ageMinutes: string;
publishedDate: string;
downloadClient: string;
downloadClientName: string;
size: string;
downloadUrl: string;
guid: string;
tmdbId: string;
protocol: string;
customFormatScore?: string;
movieMatchType: string;
releaseSource: string;
indexerFlags: string;
}
export interface DownloadFailedHistory {
message: string;
}
export interface DownloadFolderImportedHistory {
customFormatScore?: string;
droppedPath: string;
importedPath: string;
}
export interface MovieFileDeletedHistory {
customFormatScore?: string;
reason: 'Manual' | 'MissingFromDisk' | 'Upgrade';
}
export interface MovieFileRenamedHistory {
sourcePath: string;
sourceRelativePath: string;
path: string;
relativePath: string;
}
export interface DownloadIgnoredHistory {
message: string;
}
export type HistoryData =
| GrabbedHistoryData
| DownloadFailedHistory
| DownloadFolderImportedHistory
| MovieFileDeletedHistory
| MovieFileRenamedHistory
| DownloadIgnoredHistory;
export default interface History {
movieId: number;
sourceTitle: string;
@ -22,6 +77,6 @@ export default interface History {
date: string;
downloadId: string;
eventType: HistoryEventType;
data: unknown;
data: HistoryData;
id: number;
}

@ -1048,7 +1048,7 @@
"DeleteConditionMessageText": "هل أنت متأكد من أنك تريد حذف ملف تعريف الجودة {0}",
"AddAutoTagError": "غير قادر على إضافة قائمة جديدة ، يرجى المحاولة مرة أخرى.",
"ConditionUsingRegularExpressions": "يتطابق هذا الشرط مع استخدام التعبيرات العادية. لاحظ أن الأحرف {0} لها معاني خاصة وتحتاج إلى الهروب بعلامة {1}",
"DeletedReasonMissingFromDisk": "لم يتمكن Whisparr من العثور على الملف على القرص لذا تمت إزالته",
"DeletedReasonMovieMissingFromDisk": "لم يتمكن {appName} من العثور على الملف على القرص لذا تمت إزالته",
"MovieFileDeleted": "عند حذف ملف الفيلم",
"DeleteCustomFormatMessageText": "هل أنت متأكد أنك تريد حذف العلامة \"{0}\"؟",
"DeleteFormatMessageText": "هل أنت متأكد أنك تريد حذف العلامة \"{0}\"؟",

@ -1046,7 +1046,7 @@
"RemoveSelectedItemsQueueMessageText": "Наистина ли искате да премахнете {0} елемент {1} от опашката?",
"ApplyTagsHelpTextHowToApplyMovies": "Как да приложите тагове към избраните филми",
"MovieSearchResultsLoadError": "Не могат да се заредят резултати за това търсене на филм. Опитайте отново по-късно",
"DeletedReasonMissingFromDisk": "Whisparr не можа да намери файла на диска, така че той беше премахнат",
"DeletedReasonMovieMissingFromDisk": "{appName} не можа да намери файла на диска, така че той беше премахнат",
"IMDbId": "Идентификатор на TMDb",
"NotificationsSimplepushSettingsEvent": "Събития",
"MovieIsNotMonitored": "Филмът не се следи",

@ -1112,7 +1112,7 @@
"ConnectionLostReconnect": "{appName} intentarà connectar-se automàticament, o podeu fer clic a recarregar.",
"ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per a restaurar la funcionalitat.",
"DelayingDownloadUntil": "S'està retardant la baixada fins al {date} a les {time}",
"DeletedReasonMissingFromDisk": "{appName} no ha pogut trobar el fitxer al disc, de manera que el fitxer es desenllaçarà de la pel·lícula a la base de dades",
"DeletedReasonMovieMissingFromDisk": "{appName} no ha pogut trobar el fitxer al disc, de manera que el fitxer es desenllaçarà de la pel·lícula a la base de dades",
"DeletedReasonUpgrade": "S'ha suprimit el fitxer per a importar una versió millorada",
"HistoryLoadError": "No es pot carregar l'historial",
"MovieFileDeleted": "S'ha suprimit el fitxer de pel·lícula",
@ -1200,7 +1200,7 @@
"InteractiveSearchResultsFailedErrorMessage": "La cerca ha fallat per {message}. Actualitza la informació de la pel·lícula i verifica que hi hagi la informació necessària abans de tornar a cercar.",
"LogFilesLocation": "Els fitxers de registre es troben a: {location}",
"ManageDownloadClients": "Gestiona els clients de descàrrega",
"MovieGrabbedHistoryTooltip": "Pel·lícula captura de {indexer} i enviada a {downloadClient}",
"MovieGrabbedTooltip": "Pel·lícula captura de {indexer} i enviada a {downloadClient}",
"MovieFolderImportedTooltip": "Pel·lícula importada des de la carpeta de pel·lícules",
"FullColorEvents": "Esdeveniments a tot color",
"FullColorEventsHelpText": "Estil alterat per a pintar tot l'esdeveniment amb el color d'estat, en lloc de només la vora esquerra. No s'aplica a l'Agenda",
@ -1240,8 +1240,8 @@
"InfoUrl": "URL d'informació",
"InvalidUILanguage": "La vostra IU està configurada en un idioma no vàlid, corregiu-lo i deseu la configuració",
"ManageImportLists": "Gestiona les llistes d'importació",
"MovieDownloadFailedTooltip": "La baixada de la pel·lícula ha fallat",
"MovieDownloadIgnoredTooltip": "S'ha ignorat la pel·lícula baixada",
"DownloadFailedMovieTooltip": "La baixada de la pel·lícula ha fallat",
"DownloadIgnoredMovieTooltip": "S'ha ignorat la pel·lícula baixada",
"MovieFileRenamed": "S'ha canviat el nom del fitxer de pel·lícula",
"MovieImportedTooltip": "La pel·lícula s'ha baixat correctament i s'ha recollit del client de descàrrega",
"EnableProfile": "Activa el perfil",

@ -1090,7 +1090,7 @@
"BlocklistReleaseHelpText": "Zabránit {appName} v opětovném sebrání tohoto vydání pomocí RSS nebo automatického vyhledávání",
"CustomFormatJson": "Vlastní JSON formát",
"DeleteQualityProfileMessageText": "Opravdu chcete smazat profil kvality '{name}'?",
"DeletedReasonMissingFromDisk": "{appName}u se nepodařilo najít soubor na disku, proto byl soubor odpojen od filmu v databázi",
"DeletedReasonMovieMissingFromDisk": "{appName}u se nepodařilo najít soubor na disku, proto byl soubor odpojen od filmu v databázi",
"DeletedReasonUpgrade": "Soubor byl odstraněn pro import lepší verze",
"DeleteSelectedMovieFilesHelpText": "Opravdu chcete smazat vybrané soubory filmů?",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {appName}.",

@ -1067,7 +1067,7 @@
"MovieIsNotMonitored": "Film overvåges",
"DeleteReleaseProfile": "Slet forsinkelsesprofil",
"DeleteReleaseProfileMessageText": "Er du sikker på, at du vil slette kvalitetsprofilen {0}",
"DeletedReasonMissingFromDisk": "Whisparr kunne ikke finde filen på disken, så den blev fjernet",
"DeletedReasonMovieMissingFromDisk": "{appName} kunne ikke finde filen på disken, så den blev fjernet",
"ReleaseProfilesLoadError": "Kunne ikke indlæse forsinkelsesprofiler",
"SearchOnAddCollectionHelpText": "Søg efter film på denne liste, når du føjes til {appName}",
"EditConnectionImplementation": "Tilføj forbindelse - {implementationName}",

@ -1443,7 +1443,7 @@
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden",
"DownloadClientDelugeSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden",
"ListQualityProfileHelpText": "Qualitätsprofil mit dem Listemelemente hinzugefügt werden sollen",
"DeletedReasonMissingFromDisk": "{appName} konnte die Datei auf der Festplatte nicht finden, daher wurde die Verknüpfung der Datei mit der Episode in der Datenbank aufgehoben",
"DeletedReasonMovieMissingFromDisk": "{appName} konnte die Datei auf der Festplatte nicht finden, daher wurde die Verknüpfung der Datei mit der Episode in der Datenbank aufgehoben",
"ReleaseProfileTagMovieHelpText": "Veröffentlichungsprofile gelten für Künstler mit mindestens einem passenden Tag. Leer lassen, damit es für alle Künstler gilt",
"SearchForAllMissingMoviesConfirmationCount": "Bist du dir sicher, dass du nach allen '{0}' fehlenden Alben suchen willst?",
"SearchForCutoffUnmetMovies": "Suche nach allen abgeschnittenen unerfüllten Büchern",

@ -1192,7 +1192,7 @@
"ConditionUsingRegularExpressions": "Αυτή η συνθήκη ταιριάζει με τη χρήση τυπικών εκφράσεων. Λάβετε υπόψη ότι οι χαρακτήρες {0} έχουν ειδικές έννοιες και χρειάζονται διαφυγή με {1}",
"DeleteSpecificationHelpText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη συνθήκη '{name}';",
"ApplyTagsHelpTextHowToApplyMovies": "Πώς να εφαρμόσετε ετικέτες στις επιλεγμένες ταινίες",
"DeletedReasonMissingFromDisk": "Ο Whisparr δεν μπόρεσε να βρει το αρχείο στο δίσκο και έτσι καταργήθηκε",
"DeletedReasonMovieMissingFromDisk": "Ο {appName} δεν μπόρεσε να βρει το αρχείο στο δίσκο και έτσι καταργήθηκε",
"ReleaseProfileTagMovieHelpText": "Τα προφίλ κυκλοφορίας θα ισχύουν για καλλιτέχνες με τουλάχιστον μία αντίστοιχη ετικέτα. Αφήστε το κενό για να εφαρμοστεί σε όλους τους καλλιτέχνες",
"DownloadClientSettingsRecentPriority": "Προτεραιότητα πελάτη",
"MovieIsNotMonitored": "Η ταινία παρακολουθείται",

@ -365,7 +365,7 @@
"Deleted": "Deleted",
"DeletedMovieDescription": "Movie was deleted from TMDb",
"DeletedReasonManual": "File was deleted using {appName}, either manually or by another tool through the API",
"DeletedReasonMissingFromDisk": "{appName} was unable to find the file on disk so the file was unlinked from the movie in the database",
"DeletedReasonMovieMissingFromDisk": "{appName} was unable to find the file on disk so the file was unlinked from the movie in the database",
"DeletedReasonUpgrade": "File was deleted to import an upgrade",
"Destination": "Destination",
"DestinationPath": "Destination Path",
@ -541,7 +541,9 @@
"DownloadClientsLoadError": "Unable to load download clients",
"DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings",
"DownloadFailed": "Download failed",
"DownloadFailedMovieTooltip": "Movie download failed",
"DownloadIgnored": "Download Ignored",
"DownloadIgnoredMovieTooltip": "Movie Download Ignored",
"DownloadPropersAndRepacks": "Propers and Repacks",
"DownloadPropersAndRepacksHelpText": "Whether or not to automatically upgrade to Propers/Repacks",
"DownloadPropersAndRepacksHelpTextCustomFormat": "Use 'Do not Prefer' to sort by custom format score over Propers/Repacks",
@ -962,8 +964,6 @@
"MovieCollectionRootFolderMissingRootHealthCheckMessage": "Missing root folder for movie collection: {rootFolderInfo}",
"MovieDetailsNextMovie": "Movie Details: Next Movie",
"MovieDetailsPreviousMovie": "Movie Details: Previous Movie",
"MovieDownloadFailedTooltip": "Movie download failed",
"MovieDownloadIgnoredTooltip": "Movie Download Ignored",
"MovieDownloaded": "Movie Downloaded",
"MovieEditor": "Movie Editor",
"MovieExcludedFromAutomaticAdd": "Movie Excluded From Automatic Add",
@ -978,7 +978,7 @@
"MovieFolderFormatHelpText": "Used when adding a new movie or moving movies via the movie editor",
"MovieFolderImportedTooltip": "Movie imported from movie folder",
"MovieFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Movie Title:30}`) or the beginning (e.g. `{Movie Title:-30}`) are both supported.",
"MovieGrabbedHistoryTooltip": "Movie grabbed from {indexer} and sent to {downloadClient}",
"MovieGrabbedTooltip": "Movie grabbed from {indexer} and sent to {downloadClient}",
"MovieID": "Movie ID",
"MovieImported": "Movie Imported",
"MovieImportedTooltip": "Movie downloaded successfully and picked up from download client",

@ -1157,7 +1157,7 @@
"DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?",
"DisabledForLocalAddresses": "Deshabilitada para direcciones locales",
"DeletedReasonManual": "El archivo fue eliminado usando {appName}, o bien manualmente o por otra herramienta a través de la API",
"DeletedReasonMissingFromDisk": "{appName} no ha podido encontrar el archivo en el disco, por lo que se ha desvinculado de la película en la base de datos",
"DeletedReasonMovieMissingFromDisk": "{appName} no ha podido encontrar el archivo en el disco, por lo que se ha desvinculado de la película en la base de datos",
"DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada",
"DeleteSelectedMovieFilesHelpText": "¿Está seguro de que desea eliminar los archivos de película seleccionados?",
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña",
@ -1222,13 +1222,13 @@
"MovieFileRenamedTooltip": "Archivo de película renombrado",
"MovieFolderImportedTooltip": "Película importada de la carpeta de películas",
"InteractiveSearchModalHeader": "Búsqueda interactiva",
"MovieDownloadIgnoredTooltip": "Descarga de la película ignorada",
"MovieDownloadFailedTooltip": "No se ha podido descargar la película",
"DownloadIgnoredMovieTooltip": "Descarga de la película ignorada",
"DownloadFailedMovieTooltip": "No se ha podido descargar la película",
"MovieFileDeletedTooltip": "Archivo de película eliminado",
"MovieFileRenamed": "Archivo de película renombrado",
"LanguagesLoadError": "No es posible cargar los idiomas",
"DownloadClientQbittorrentSettingsContentLayout": "Diseño del contenido",
"MovieGrabbedHistoryTooltip": "Película capturada de {indexer} y enviada a {downloadClient}",
"MovieGrabbedTooltip": "Película capturada de {indexer} y enviada a {downloadClient}",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usar el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crear una subcarpeta (qBittorrent 4.3.2+)",
"MovieImported": "Película importada",
"MovieImportedTooltip": "Película descargada correctamente y obtenida del cliente de descargas",

@ -1216,7 +1216,7 @@
"DeletedReasonManual": "Tiedosto poistettiin käyttöliittymän kautta",
"DeletedReasonUpgrade": "Tiedosto poistettiin päivitetyn version tuomiseksi",
"InteractiveImportNoMovie": "Elokuva on valittava jokaiselle valitulle tiedostolle.",
"MovieGrabbedHistoryTooltip": "Elokuva kaapattiin lähteestä {indexer} ja välitettiin lataajalle {downloadClient}",
"MovieGrabbedTooltip": "Elokuva kaapattiin lähteestä {indexer} ja välitettiin lataajalle {downloadClient}",
"CloneCondition": "Monista ehto",
"AutomaticAdd": "Automaattinen lisäys",
"AutoTagging": "Automaattinen tunnistemerkintä",
@ -1245,7 +1245,7 @@
"InteractiveImportNoFilesFound": "Valitusta kansiosta ei löytynyt videotiedostoja.",
"ManualGrab": "Manuaalinen kaappaus",
"Complete": "Kokonaiset",
"DeletedReasonMissingFromDisk": "{appName} ei löytänyt tiedostoa levyltä, joten sen kytkös kirjaston elokuvaan poistettiin.",
"DeletedReasonMovieMissingFromDisk": "{appName} ei löytänyt tiedostoa levyltä, joten sen kytkös kirjaston elokuvaan poistettiin.",
"ShowImdbRatingHelpText": "Näytä IMDb-arvio julisteen alla.",
"ShowRottenTomatoesRating": "Näytä Tomato-arvio",
"ShowRottenTomatoesRatingHelpText": "Näytä Tomato-arvio julisteen alla.",
@ -1415,7 +1415,7 @@
"CustomFormatsSpecificationRegularExpression": "Säännöllinen lauseke",
"False": "Epätosi",
"CustomFormatsSpecificationRegularExpressionHelpText": "Mukautetun muodon säännöllisen lausekkeen kirjainkokoa ei huomioida.",
"MovieDownloadIgnoredTooltip": "Elokuvalataus ohitettiin",
"DownloadIgnoredMovieTooltip": "Elokuvalataus ohitettiin",
"MovieFolderImportedTooltip": "Elokuva tuotiin elokuvakansiosta",
"True": "Tosi",
"SkipRedownloadHelpText": "Estää {appName}ia lataamasta kohteelle vaihtoehtoista julkaisua.",
@ -1460,7 +1460,7 @@
"NotificationsValidationUnableToConnectToService": "Palvelua {serviceName} ei tavoiteta.",
"NotificationsValidationUnableToSendTestMessage": "Testiviestin lähetys ei onnistu: {exceptionMessage}",
"NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.",
"MovieDownloadFailedTooltip": "Elokuvan lataus epäonnistui",
"DownloadFailedMovieTooltip": "Elokuvan lataus epäonnistui",
"MovieFileDeleted": "Elokuvatiedosto poistettiin",
"MovieFileDeletedTooltip": "Elokuvatiedosto poistettiin",
"ThereWasAnErrorLoadingThisItem": "Virhe ladattaessa kohdetta",

@ -1216,7 +1216,7 @@
"AutoTaggingLoadError": "Impossible de charger le marquage automatique",
"DelayingDownloadUntil": "Retarder le téléchargement jusqu'au {date} à {time}",
"DeleteSelectedMovieFilesHelpText": "Voulez-vous vraiment supprimer les fichiers vidéo sélectionnés ?",
"DeletedReasonMissingFromDisk": "{appName} n'a pas pu trouver le fichier sur le disque, il a donc été supprimé dans la base de données",
"DeletedReasonMovieMissingFromDisk": "{appName} n'a pas pu trouver le fichier sur le disque, il a donc été supprimé dans la base de données",
"DeletedReasonUpgrade": "Le fichier a été supprimé pour importer une mise à niveau",
"OrganizeLoadError": "Erreur lors du chargement des aperçus",
"EditImportListImplementation": "Modifier la liste d'importation - {implementationName}",
@ -1285,7 +1285,7 @@
"MovieFileRenamed": "Fichier vidéo renommé",
"MovieFileRenamedTooltip": "Fichier vidéo renommé",
"MovieFolderImportedTooltip": "Film importé du dossier de film",
"MovieGrabbedHistoryTooltip": "Film récupéré de {indexer} et envoyé à {downloadClient}",
"MovieGrabbedTooltip": "Film récupéré de {indexer} et envoyé à {downloadClient}",
"RemoveQueueItem": "Retirer - {sourceTitle}",
"OverrideGrabModalTitle": "Remplacer et récupérer - {title}",
"RemoveTagsAutomaticallyHelpText": "Supprimez automatiquement les étiquettes si les conditions ne sont pas remplies",
@ -1296,8 +1296,8 @@
"SetReleaseGroupModalTitle": "{modalTitle} Définir le groupe de versions",
"CountImportListsSelected": "{count} liste(s) d'importation sélectionnée(s)",
"InteractiveSearchResultsFailedErrorMessage": "La recherche a échoué car il s'agit d'un {message}. Essayez d'actualiser les informations sur le film et vérifiez que les informations nécessaires sont présentes avant de lancer une nouvelle recherche.",
"MovieDownloadFailedTooltip": "Le téléchargement du film a échoué",
"MovieDownloadIgnoredTooltip": "Téléchargement de film ignoré",
"DownloadFailedMovieTooltip": "Le téléchargement du film a échoué",
"DownloadIgnoredMovieTooltip": "Téléchargement de film ignoré",
"SkipRedownloadHelpText": "Empêche {appName} d'essayer de télécharger une version alternative pour cet élément",
"QueueFilterHasNoItems": "Le filtre de file d'attente sélectionné ne contient aucun élément",
"EnableProfile": "Activer profil",

@ -1100,7 +1100,7 @@
"NotificationsSimplepushSettingsEvent": "אירועים",
"RemotePathMappingCheckFilesLocalWrongOSPath": "אתה משתמש בדוקר; קליינט ההורדות {downloadClientName} שם הורדות ב-{path} אבל הנתיב לא תקין {osName}. בחן מחדש את ניתוב התיקיות והגדרות קליינט ההורדות.",
"RemotePathMappingCheckLocalWrongOSPath": "אתה משתמש בדוקר; קליינט ההורדות {downloadClientName} שם הורדות ב-{path} אבל הנתיב לא תקין {osName}. בחן מחדש את ניתוב התיקיות והגדרות קליינט ההורדות.",
"DeletedReasonMissingFromDisk": "Whisparr לא הצליח למצוא את הקובץ בדיסק ולכן הוא הוסר",
"DeletedReasonMovieMissingFromDisk": "{appName} לא הצליח למצוא את הקובץ בדיסק ולכן הוא הוסר",
"AddDelayProfileError": "לא ניתן להוסיף פרופיל איכות חדש, נסה שוב.",
"MovieFileDeletedTooltip": "במחיקת קובץ הסרט",
"DeleteMovieFolders": "מחק את תיקיית הסרטים",

@ -1047,7 +1047,7 @@
"MovieIsNotMonitored": "मूवी अनमनी है",
"DownloadClientSettingsRecentPriority": "ग्राहक प्राथमिकता",
"DeleteSelectedMovieFilesHelpText": "क्या आप वाकई चयनित मूवी फ़ाइलों को हटाना चाहते हैं?",
"DeletedReasonMissingFromDisk": "Whisparr डिस्क पर फ़ाइल खोजने में असमर्थ था इसलिए इसे हटा दिया गया था",
"DeletedReasonMovieMissingFromDisk": "{appName} डिस्क पर फ़ाइल खोजने में असमर्थ था इसलिए इसे हटा दिया गया था",
"MovieFileDeleted": "मूवी फ़ाइल डिलीट पर",
"DeleteCustomFormatMessageText": "क्या आप वाकई '{0}' टैग हटाना चाहते हैं?",
"DeleteFormatMessageText": "क्या आप वाकई '{0}' टैग हटाना चाहते हैं?",

@ -1151,7 +1151,7 @@
"DeleteAutoTag": "Automatikus címke törlése",
"DeleteSelectedDownloadClients": "Letöltési kliens(ek) törlése",
"DeleteSelectedIndexersMessageText": "Biztosan törölni szeretne {count} kiválasztott indexelőt?",
"DeletedReasonMissingFromDisk": "A(z) {appName} nem találta a fájlt a lemezen, ezért a fájlt leválasztották a filmről az adatbázisban",
"DeletedReasonMovieMissingFromDisk": "A(z) {appName} nem találta a fájlt a lemezen, ezért a fájlt leválasztották a filmről az adatbázisban",
"EditIndexerImplementation": "Indexelő szerkesztése {implementationName}",
"FailedToFetchUpdates": "Nem sikerült lekérni a frissítéseket",
"FormatAgeDay": "nap",
@ -1418,7 +1418,7 @@
"DownloadClientSettingsOlderPriorityMovieHelpText": "Elsőbbség a 14 nappal ezelőtt sugárzott epizódok megragadásánál",
"MovieImportedTooltip": "Az epizód letöltése sikeresen megtörtént, és a letöltés kliensből lett letöltve",
"SearchForCutoffUnmetMoviesConfirmationCount": "Biztosan megkeresi az összes {totalRecords} hiányzó epizódot?",
"MovieGrabbedHistoryTooltip": "Az epizódot letöltötte a(z) {indexer} és elküldte a(z) {downloadClient} számára",
"MovieGrabbedTooltip": "Az epizódot letöltötte a(z) {indexer} és elküldte a(z) {downloadClient} számára",
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Választható hely a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Aria2 hely használatához",
"DownloadClientDelugeSettingsDirectoryHelpText": "Választható hely a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Aria2 hely használatához",
"MovieFileDeletedTooltip": "A filmfájl törléséhez",

@ -1055,7 +1055,7 @@
"NotificationsSimplepushSettingsEvent": "Viðburðir",
"DeleteSelectedMovieFilesHelpText": "Ertu viss um að þú viljir eyða völdum kvikmyndaskrám?",
"AutoRedownloadFailed": "Niðurhal mistókst",
"DeletedReasonMissingFromDisk": "Whisparr gat ekki fundið skrána á disknum svo hún var fjarlægð",
"DeletedReasonMovieMissingFromDisk": "{appName} gat ekki fundið skrána á disknum svo hún var fjarlægð",
"DownloadClientSettingsRecentPriority": "Forgangur viðskiptavinar",
"AddDelayProfileError": "Ekki er hægt að bæta við nýjum gæðaprófíl, reyndu aftur.",
"MovieFileDeletedTooltip": "Á Eyða kvikmyndaskrá",

@ -1162,7 +1162,7 @@
"ReleaseProfilesLoadError": "Non riesco a caricare i profili di ritardo",
"ReleaseGroups": "Gruppo Release",
"DeleteReleaseProfileMessageText": "Sicuro di voler cancellare il profilo di qualità {0}",
"DeletedReasonMissingFromDisk": "Whisparr non è riuscito a trovare il file sul disco, quindi è stato rimosso",
"DeletedReasonMovieMissingFromDisk": "{appName} non è riuscito a trovare il file sul disco, quindi è stato rimosso",
"DeleteQualityProfileMessageText": "Sicuro di voler cancellare il profilo di qualità '{name}'?",
"RemotePathMappingCheckFilesLocalWrongOSPath": "Stai utilizzando docker; Il client di download {downloadClientName} riporta files in {path} ma questo non è un percorso valido {osName}. Controlla la mappa dei percorsi remoti e le impostazioni del client di download.",
"AutoTaggingNegateHelpText": "Se selezionato, il formato personalizzato non verrà applicato a questa {0} condizione.",
@ -1231,7 +1231,7 @@
"ManageIndexers": "Gestisci Indicizzatori",
"ManageLists": "Gestisci Liste",
"Menu": "Menu",
"MovieDownloadFailedTooltip": "Download del film fallito",
"DownloadFailedMovieTooltip": "Download del film fallito",
"Never": "Mai",
"NotificationsSettingsWebhookMethod": "Metodo",
"NotificationsTraktSettingsAuthenticateWithTrakt": "Autentica con Trakt",
@ -1284,7 +1284,7 @@
"Letterboxd": "Letterboxd",
"MissingLoadError": "Errore caricando elementi mancanti",
"MonitorCollection": "Monitora Collezione",
"MovieDownloadIgnoredTooltip": "Download del Film Ignorato",
"DownloadIgnoredMovieTooltip": "Download del Film Ignorato",
"MovieIsNotAvailable": "Film non disponibile",
"MovieIsPopular": "Film Popolare su TMDb",
"MovieIsTrending": "Film in Tendenza su TMDb",

@ -1049,7 +1049,7 @@
"MovieFileDeleted": "ムービーファイルの削除について",
"SearchOnAddCollectionHelpText": "{appName}に追加されたら、このリストで映画を検索します",
"MovieIsNotMonitored": "映画は監視されていません",
"DeletedReasonMissingFromDisk": "Whisparrはディスク上でファイルを見つけることができなかったため、削除されました",
"DeletedReasonMovieMissingFromDisk": "{appName}はディスク上でファイルを見つけることができなかったため、削除されました",
"IMDbId": "TMDbID",
"DeleteSelectedMovieFilesHelpText": "選択したムービーファイルを削除してもよろしいですか?",
"NotificationsSimplepushSettingsEvent": "イベント",

@ -1039,7 +1039,7 @@
"RemoveSelectedBlocklistMessageText": "블랙리스트에서 선택한 항목을 제거 하시겠습니까?",
"Yes": "예",
"ApplyTagsHelpTextHowToApplyMovies": "선택한 동영상에 태그를 적용하는 방법",
"DeletedReasonMissingFromDisk": "Whisparr가 디스크에서 파일을 찾을 수 없어 제거되었습니다.",
"DeletedReasonMovieMissingFromDisk": "{appName}가 디스크에서 파일을 찾을 수 없어 제거되었습니다.",
"ShowUnknownMovieItemsHelpText": "대기열에 영화가없는 항목을 표시합니다. 여기에는 제거 된 영화 또는 {appName} 카테고리의 다른 항목이 포함될 수 있습니다.",
"DeleteSelectedMovieFilesHelpText": "선택한 동영상 파일을 삭제 하시겠습니까?",
"DownloadClientSettingsRecentPriority": "클라이언트 우선 순위",

@ -1185,7 +1185,7 @@
"FormatAgeMinute": "minuten",
"EditConnectionImplementation": "Voeg connectie toe - {implementationName}",
"FormatAgeHour": "Uren",
"DeletedReasonMissingFromDisk": "Whisparr kon het bestand niet vinden op de schijf dus werd het verwijderd",
"DeletedReasonMovieMissingFromDisk": "{appName} kon het bestand niet vinden op de schijf dus werd het verwijderd",
"DeleteReleaseProfile": "Verwijder Vertragingsprofiel",
"DeleteReleaseProfileMessageText": "Bent u zeker dat u het kwaliteitsprofiel {0} wilt verwijderen",
"DownloadClientSettingsRecentPriority": "Client Prioriteit",

@ -1151,7 +1151,7 @@
"MovieSearchResultsLoadError": "Nie można załadować wyników dla tego wyszukiwania filmów. Spróbuj ponownie później",
"ShowUnknownMovieItemsHelpText": "Pokaż elementy bez filmu w kolejce. Może to obejmować usunięte filmy lub cokolwiek innego w kategorii Lidarr",
"EditConnectionImplementation": "Dodaj Connection - {implementationName}",
"DeletedReasonMissingFromDisk": "Whisparr nie mógł znaleźć pliku na dysku, więc został usunięty",
"DeletedReasonMovieMissingFromDisk": "{appName} nie mógł znaleźć pliku na dysku, więc został usunięty",
"DeleteSelectedMovieFilesHelpText": "Czy na pewno chcesz usunąć wybrane pliki filmowe?",
"IMDbId": "Identyfikator TMDb",
"AddDelayProfileError": "Nie można dodać nowego profilu opóźnienia, spróbuj później.",

@ -1135,7 +1135,7 @@
"DeleteSelectedImportListsMessageText": "Tem a certeza de que pretende eliminar a(s) lista(s) de {count} importação selecionada(s)?",
"DeleteSelectedMovieFilesHelpText": "Tem a certeza de que pretende apagar os ficheiros de filmes seleccionados?",
"DeletedReasonManual": "O ficheiro foi eliminado através da IU",
"DeletedReasonMissingFromDisk": "O {appName} não conseguiu encontrar o ficheiro no disco, pelo que o ficheiro foi desvinculado do filme na base de dados",
"DeletedReasonMovieMissingFromDisk": "O {appName} não conseguiu encontrar o ficheiro no disco, pelo que o ficheiro foi desvinculado do filme na base de dados",
"DeletedReasonUpgrade": "O ficheiro foi eliminado para importar uma atualização",
"DisabledForLocalAddresses": "Desativado para Endereços Locais",
"DownloadClientsLoadError": "Não foi possível carregar os clientes de transferências",

@ -1238,8 +1238,8 @@
"UnknownEventTooltip": "Evento desconhecido",
"DeletedReasonManual": "O arquivo foi excluído usando {appName} manualmente ou por outra ferramenta por meio da API",
"FormatAgeDay": "dia",
"MovieDownloadFailedTooltip": "Falha no download do filme",
"MovieDownloadIgnoredTooltip": "Download do Filme Ignorado",
"DownloadFailedMovieTooltip": "Falha no download do filme",
"DownloadIgnoredMovieTooltip": "Download do Filme Ignorado",
"NoHistoryFound": "Nenhum histórico encontrado",
"QueueLoadError": "Falha ao carregar a fila",
"BlocklistLoadError": "Não foi possível carregar a lista de bloqueio",
@ -1252,7 +1252,7 @@
"HistoryLoadError": "Não foi possível carregar o histórico",
"InfoUrl": "URL da info",
"MovieImported": "Filme Importado",
"MovieGrabbedHistoryTooltip": "Filme obtido de {indexer} e enviado para {downloadClient}",
"MovieGrabbedTooltip": "Filme obtido de {indexer} e enviado para {downloadClient}",
"MovieImportedTooltip": "Filme baixado com sucesso e obtido no cliente de download",
"PendingDownloadClientUnavailable": "Pendente - O cliente de download não está disponível",
"FormatAgeDays": "dias",
@ -1279,7 +1279,7 @@
"TablePageSize": "Tamanho da Página",
"TablePageSizeHelpText": "Número de itens a serem exibidos em cada página",
"TablePageSizeMinimum": "O tamanho da página precisa ser de pelo menos {minimumValue}",
"DeletedReasonMissingFromDisk": "O {appName} não conseguiu encontrar o arquivo no disco, então o arquivo foi desvinculado do filme no banco de dados",
"DeletedReasonMovieMissingFromDisk": "O {appName} não conseguiu encontrar o arquivo no disco, então o arquivo foi desvinculado do filme no banco de dados",
"FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}",
"RemoveSelectedBlocklistMessageText": "Tem certeza de que deseja remover os itens selecionados da lista de bloqueio?",
"ShowUnknownMovieItemsHelpText": "Mostrar itens sem filme na fila. Isso pode incluir filmes removidos ou qualquer outra coisa na categoria do {appName}",

@ -1035,14 +1035,14 @@
"MovieFileRenamed": "Fișier de film redenumit",
"MovieFileDeletedTooltip": "Fișier de film șters",
"MovieFileDeleted": "Fișier de film șters",
"MovieDownloadIgnoredTooltip": "Descărcare film ignorată",
"DownloadIgnoredMovieTooltip": "Descărcare film ignorată",
"MovieImported": "Film importat",
"MovieGrabbedHistoryTooltip": "Film preluat de la {indexer} și trimis către {downloadClient}",
"MovieGrabbedTooltip": "Film preluat de la {indexer} și trimis către {downloadClient}",
"FormatAgeMinutes": "minute",
"UnknownEventTooltip": "Eveniment necunoscut",
"TablePageSize": "Mărimea Paginii",
"MovieFileRenamedTooltip": "Fișier de film redenumit",
"MovieDownloadFailedTooltip": "Descărcarea filmului a eșuat",
"DownloadFailedMovieTooltip": "Descărcarea filmului a eșuat",
"FullColorEvents": "Evenimente pline de culoare",
"BlocklistReleaseHelpText": "Împiedică {appName} să apuce automat această versiune din nou",
"BlocklistLoadError": "Imposibil de încărcat lista neagră",
@ -1070,7 +1070,7 @@
"ReleaseProfilesLoadError": "Nu se pot încărca profilurile",
"PendingDownloadClientUnavailable": "În așteptare - Clientul de descărcare nu este disponibil",
"MovieImportedTooltip": "Filmul a fost descărcat cu succes și preluat de la clientul de descărcare",
"DeletedReasonMissingFromDisk": "{appName} nu a putut găsi fișierul de pe disc, așa că a fost eliminat",
"DeletedReasonMovieMissingFromDisk": "{appName} nu a putut găsi fișierul de pe disc, așa că a fost eliminat",
"AddConnection": "Adăugați conexiune",
"AddConnectionImplementation": "Adăugați conexiune - {implementationName}",
"AddDownloadClientImplementation": "Adăugați client de descărcare - {implementationName}",

@ -1263,7 +1263,7 @@
"Label": "Метка",
"DelayProfileMovieTagsHelpText": "Применимо к фильмам с хотя бы одним подходящим тегом",
"Release": "Релиз",
"DeletedReasonMissingFromDisk": "{appName} не смог найти файл на диске, поэтому файл был откреплён от фильма в базе данных",
"DeletedReasonMovieMissingFromDisk": "{appName} не смог найти файл на диске, поэтому файл был откреплён от фильма в базе данных",
"MovieIsNotMonitored": "Фильм не отслеживается",
"MovieFileDeleted": "При удалении файла фильма",
"DeleteReleaseProfile": "Удалить профиль релиза",
@ -1641,7 +1641,7 @@
"EditionFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Series Title:30}`), так и с начала (например, `{Series Title:-30}`).",
"NotificationsCustomScriptValidationFileDoesNotExist": "Файл не существует",
"NotificationsGotifySettingsServerHelpText": "URL-адрес сервера Gotify, включая http(s):// и порт, если необходимо",
"MovieGrabbedHistoryTooltip": "Эпизод получен из {indexer} и отправлен в {downloadClient}",
"MovieGrabbedTooltip": "Эпизод получен из {indexer} и отправлен в {downloadClient}",
"NotificationsPlexValidationNoMovieLibraryFound": "Требуется хотя бы одна библиотека c сериалами",
"SearchForAllMissingMovies": "Искать все недостающие эпизоды",
"SearchForAllMissingMoviesConfirmationCount": "Вы уверены, что хотите найти все ({totalRecords}) недостающие эпизоды ?",

@ -1069,7 +1069,7 @@
"ConditionUsingRegularExpressions": "Detta villkor matchar användningen av reguljära uttryck. Observera att tecknen {0} har speciella betydelser och behöver fly med ett {1}",
"MovieFileDeleted": "På filmfil Ta bort",
"DeleteCustomFormatMessageText": "Är du säker på att du vill radera taggen '{0}'?",
"DeletedReasonMissingFromDisk": "Whisparr kunde inte hitta filen på disken så den togs bort",
"DeletedReasonMovieMissingFromDisk": "{appName} kunde inte hitta filen på disken så den togs bort",
"DeleteSelectedMovieFilesHelpText": "Är du säker på att du vill radera de markerade filmfilerna?",
"MovieIsNotMonitored": "FIlmen är bevakad",
"SearchForAllMissingMovies": "Sök efter alla saknade böcker",

@ -1052,7 +1052,7 @@
"IMDbId": "รหัส TMDb",
"MovieFileDeleted": "บน Movie File Delete",
"AddDelayProfileError": "ไม่สามารถเพิ่มโปรไฟล์คุณภาพใหม่ได้โปรดลองอีกครั้ง",
"DeletedReasonMissingFromDisk": "Whisparr ไม่พบไฟล์บนดิสก์ดังนั้นจึงถูกลบออก",
"DeletedReasonMovieMissingFromDisk": "{appName} ไม่พบไฟล์บนดิสก์ดังนั้นจึงถูกลบออก",
"DownloadClientSettingsRecentPriority": "ลำดับความสำคัญของลูกค้า",
"DeleteSelectedMovieFilesHelpText": "แน่ใจไหมว่าต้องการลบไฟล์ภาพยนตร์ที่เลือก",
"ShowUnknownMovieItemsHelpText": "แสดงรายการที่ไม่มีภาพยนตร์อยู่ในคิว ซึ่งอาจรวมถึงภาพยนตร์ที่ถูกนำออกหรือสิ่งอื่นใดในหมวดหมู่ของ Lidarr",

@ -1114,7 +1114,7 @@
"DeleteRootFolderMessageText": "'{path}' kök klasörünü silmek istediğinizden emin misiniz?",
"DeleteSelectedIndexersMessageText": "Seçilen {count} dizinleyiciyi silmek istediğinizden emin misiniz?",
"Destination": "Hedef",
"DeletedReasonMissingFromDisk": "{appName} dosyayı diskte bulamadığından dosyanın veritabanındaki filmle bağlantısı kaldırıldı",
"DeletedReasonMovieMissingFromDisk": "{appName} dosyayı diskte bulamadığından dosyanın veritabanındaki filmle bağlantısı kaldırıldı",
"DownloadClientDelugeTorrentStateError": "Deluge bir hata bildiriyor",
"DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi",
"DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.",
@ -1287,8 +1287,8 @@
"ListRefreshInterval": "Liste Yenileme Aralığı",
"InteractiveImportNoImportMode": "Bir içe aktarma modu seçilmelidir",
"MonitorCollection": "Takip Etme Koleksiyonu",
"MovieDownloadFailedTooltip": "Film indirilemedi",
"MovieDownloadIgnoredTooltip": "Film İndirme Yoksayıldı",
"DownloadFailedMovieTooltip": "Film indirilemedi",
"DownloadIgnoredMovieTooltip": "Film İndirme Yoksayıldı",
"NotificationsAppriseSettingsServerUrlHelpText": "Gerekiyorsa http(s):// ve bağlantı noktasını içeren Apprise sunucu URL'si",
"NotificationsAppriseSettingsTags": "Apprise Etiketler",
"NotificationsEmailSettingsUseEncryption": "Şifreleme Kullan",
@ -1341,7 +1341,7 @@
"IndexerDownloadClientHealthCheckMessage": "Geçersiz indirme istemcilerine sahip dizinleyiciler: {indexerNames}.",
"MovieCollectionRootFolderMissingRootHealthCheckMessage": "Film koleksiyonu için eksik kök klasör: {rootFolderInfo}",
"NoImportListsFound": "İçe aktarma listesi bulunamadı",
"MovieGrabbedHistoryTooltip": "Film {indexer}'dan alındı ve {downloadClient}'a gönderildi",
"MovieGrabbedTooltip": "Film {indexer}'dan alındı ve {downloadClient}'a gönderildi",
"NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor",
"FormatAgeHour": "saat",
"GrabId": "ID'den Yakala",

@ -1103,7 +1103,7 @@
"AddConnection": "Додати Підключення",
"TablePageSizeHelpText": "Кількість елементів для показу на кожній сторінці",
"ConnectionLostToBackend": "{appName} втратив з’єднання з серверною частиною, і його потрібно перезавантажити, щоб відновити роботу.",
"DeletedReasonMissingFromDisk": "{appName} не зміг знайти файл на диску, тому файл було від’єднано від фільму в базі даних",
"DeletedReasonMovieMissingFromDisk": "{appName} не зміг знайти файл на диску, тому файл було від’єднано від фільму в базі даних",
"FormatAgeHours": "Години",
"FormatAgeMinute": "Хвилин",
"FormatAgeMinutes": "Хвилин",

@ -1046,7 +1046,7 @@
"DeleteSelectedMovieFilesHelpText": "Bạn có chắc chắn muốn xóa các tệp phim đã chọn không?",
"DeleteCustomFormatMessageText": "Bạn có chắc chắn muốn xóa thẻ '{0}' không?",
"DeleteQualityProfileMessageText": "Bạn có chắc chắn muốn xóa cấu hình chất lượng không {0}",
"DeletedReasonMissingFromDisk": "Whisparr không thể tìm thấy tệp trên đĩa nên nó đã bị xóa",
"DeletedReasonMovieMissingFromDisk": "{appName} không thể tìm thấy tệp trên đĩa nên nó đã bị xóa",
"MovieIsNotMonitored": "Phim không được giám sát",
"SearchOnAddCollectionHelpText": "Tìm kiếm phim trong danh sách này khi được thêm vào {appName}",
"DeleteFormatMessageText": "Bạn có chắc chắn muốn xóa thẻ '{0}' không?",

@ -1205,8 +1205,8 @@
"InfoUrl": "信息 URL",
"InvalidUILanguage": "您的UI设置为无效语言请纠正并保存设置",
"LanguagesLoadError": "无法加载语言",
"MovieDownloadFailedTooltip": "电影下载失败",
"MovieDownloadIgnoredTooltip": "电影下载被忽略",
"DownloadFailedMovieTooltip": "电影下载失败",
"DownloadIgnoredMovieTooltip": "电影下载被忽略",
"MovieFileDeleted": "电影文件已删除",
"MovieFileRenamed": "电影文件已重命名",
"MovieFileRenamedTooltip": "电影文件已重命名",
@ -1217,7 +1217,7 @@
"AppUpdatedVersion": "{appName} 已经更新到版本 {version} ,重新加载 {appName} 使更新生效",
"ConnectionLostReconnect": "{appName} 将会尝试自动连接,您也可以点击下方的重新加载。",
"ConnectionLostToBackend": "{appName}失去了与后端的连接,需要重新加载以恢复功能。",
"DeletedReasonMissingFromDisk": "{appName} 无法在磁盘上找到该文件,因此该文件已与数据库中的电影解除链接",
"DeletedReasonMovieMissingFromDisk": "{appName} 无法在磁盘上找到该文件,因此该文件已与数据库中的电影解除链接",
"EditDownloadClientImplementation": "编辑下载客户端- {implementationName}",
"EditIndexerImplementation": "编辑索引器- {implementationName}",
"GrabId": "抓取ID",
@ -1225,7 +1225,7 @@
"IMDbId": "IMDb Id",
"InteractiveImportNoMovie": "每个选中的文件必须选择对应的电影",
"MovieFileDeletedTooltip": "电影文件已删除",
"MovieGrabbedHistoryTooltip": "从 {indexer} 抓取电影并发送到 {downloadClient}",
"MovieGrabbedTooltip": "从 {indexer} 抓取电影并发送到 {downloadClient}",
"IndexerDownloadClientHealthCheckMessage": "使用无效下载客户端的索引器:{indexerNames}。",
"FullColorEvents": "全彩事件",
"InteractiveImportNoFilesFound": "在所选文件夹中找不到视频文件",

Loading…
Cancel
Save