mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-09 05:40:53 +02:00
Use react-query for series import
This commit is contained in:
parent
25fb4c4d7a
commit
ad57cf4b5d
17 changed files with 420 additions and 686 deletions
|
|
@ -38,11 +38,7 @@ function AddNewSeries() {
|
|||
setIsFetching(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isFetching: isFetchingApi,
|
||||
error,
|
||||
data = [],
|
||||
} = useLookupSeries(query);
|
||||
const { isFetching: isFetchingApi, error, data } = useLookupSeries(query);
|
||||
|
||||
useEffect(() => {
|
||||
setIsFetching(isFetchingApi);
|
||||
|
|
|
|||
|
|
@ -12,18 +12,25 @@ interface AddSeriesPayload
|
|||
'monitor' | 'searchForMissingEpisodes' | 'searchForCutoffUnmetEpisodes'
|
||||
> {}
|
||||
|
||||
export const useLookupSeries = (query: string) => {
|
||||
return useApiQuery<AddSeries[]>({
|
||||
const DEFAULT_SERIES: AddSeries[] = [];
|
||||
|
||||
export const useLookupSeries = (query: string, isEnabled = true) => {
|
||||
const result = useApiQuery<AddSeries[]>({
|
||||
path: '/series/lookup',
|
||||
queryParams: {
|
||||
term: query,
|
||||
},
|
||||
queryOptions: {
|
||||
enabled: !!query,
|
||||
enabled: isEnabled && !!query,
|
||||
// Disable refetch on window focus to prevent refetching when the user switch tabs
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? DEFAULT_SERIES,
|
||||
};
|
||||
};
|
||||
|
||||
export const useAddSeries = () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import {
|
||||
setAddSeriesOption,
|
||||
|
|
@ -13,13 +13,12 @@ import PageContent from 'Components/Page/PageContent';
|
|||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import useRootFolders, { useRootFolder } from 'RootFolder/useRootFolders';
|
||||
import { clearImportSeries } from 'Store/Actions/importSeriesActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportSeriesFooter from './ImportSeriesFooter';
|
||||
import { clearImportSeries } from './importSeriesStore';
|
||||
import ImportSeriesTable from './ImportSeriesTable';
|
||||
|
||||
function ImportSeries() {
|
||||
const dispatch = useDispatch();
|
||||
const { rootFolderId: rootFolderIdString } = useParams<{
|
||||
rootFolderId: string;
|
||||
}>();
|
||||
|
|
@ -68,9 +67,9 @@ function ImportSeries() {
|
|||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(clearImportSeries());
|
||||
clearImportSeries();
|
||||
};
|
||||
}, [rootFolderId, dispatch]);
|
||||
}, [rootFolderId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -79,13 +78,15 @@ function ImportSeries() {
|
|||
) {
|
||||
setAddSeriesOption('qualityProfileId', qualityProfiles[0].id);
|
||||
}
|
||||
}, [defaultQualityProfileId, qualityProfiles, dispatch]);
|
||||
}, [defaultQualityProfileId, qualityProfiles]);
|
||||
|
||||
return (
|
||||
<SelectProvider items={items}>
|
||||
<PageContent title={translate('ImportSeries')}>
|
||||
<PageContentBody ref={scrollerRef}>
|
||||
{rootFoldersFetching ? <LoadingIndicator /> : null}
|
||||
{rootFoldersFetching && !rootFoldersFetched ? (
|
||||
<LoadingIndicator />
|
||||
) : null}
|
||||
|
||||
{!rootFoldersFetching && !!rootFoldersError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
|
|
@ -103,20 +104,14 @@ function ImportSeries() {
|
|||
) : null}
|
||||
|
||||
{!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersFetched &&
|
||||
!!unmappedFolders.length &&
|
||||
scrollerRef.current ? (
|
||||
<ImportSeriesTable
|
||||
unmappedFolders={unmappedFolders}
|
||||
scrollerRef={scrollerRef}
|
||||
/>
|
||||
<ImportSeriesTable items={items} scrollerRef={scrollerRef} />
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
|
||||
{!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
!!unmappedFolders.length ? (
|
||||
{!rootFoldersError && rootFoldersFetched && !!unmappedFolders.length ? (
|
||||
<ImportSeriesFooter />
|
||||
) : null}
|
||||
</PageContent>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
AddSeriesOptions,
|
||||
setAddSeriesOption,
|
||||
useAddSeriesOptions,
|
||||
} from 'AddSeries/addSeriesOptionsStore';
|
||||
import { useSelect } from 'App/Select/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import Icon from 'Components/Icon';
|
||||
|
|
@ -17,20 +15,22 @@ import PageContentFooter from 'Components/Page/PageContentFooter';
|
|||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { SeriesMonitor, SeriesType } from 'Series/Series';
|
||||
import {
|
||||
cancelLookupSeries,
|
||||
importSeries,
|
||||
lookupUnsearchedSeries,
|
||||
setImportSeriesValue,
|
||||
} from 'Store/Actions/importSeriesActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import {
|
||||
ImportSeriesItem,
|
||||
startProcessing,
|
||||
stopProcessing,
|
||||
updateImportSeriesItem,
|
||||
useImportSeriesItems,
|
||||
useLookupQueueHasItems,
|
||||
} from './importSeriesStore';
|
||||
import { useImportSeries } from './useImportSeries';
|
||||
import styles from './ImportSeriesFooter.css';
|
||||
|
||||
type MixedType = 'mixed';
|
||||
|
||||
function ImportSeriesFooter() {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
|
|
@ -38,9 +38,8 @@ function ImportSeriesFooter() {
|
|||
seasonFolder: defaultSeasonFolder,
|
||||
} = useAddSeriesOptions();
|
||||
|
||||
const { isLookingUpSeries, isImporting, items, importError } = useSelector(
|
||||
(state: AppState) => state.importSeries
|
||||
);
|
||||
const items = useImportSeriesItems();
|
||||
const isLookingUpSeries = useLookupQueueHasItems();
|
||||
|
||||
const [monitor, setMonitor] = useState<SeriesMonitor | MixedType>(
|
||||
defaultMonitor
|
||||
|
|
@ -55,7 +54,9 @@ function ImportSeriesFooter() {
|
|||
defaultSeasonFolder
|
||||
);
|
||||
|
||||
const { selectedCount, getSelectedIds } = useSelect();
|
||||
const { selectedCount, getSelectedIds } = useSelect<ImportSeriesItem>();
|
||||
|
||||
const { importSeries, isImporting, importError } = useImportSeries();
|
||||
|
||||
const {
|
||||
hasUnsearchedItems,
|
||||
|
|
@ -87,7 +88,7 @@ function ImportSeriesFooter() {
|
|||
isSeasonFolderMixed = true;
|
||||
}
|
||||
|
||||
if (!item.isPopulated) {
|
||||
if (!item.hasSearched) {
|
||||
hasUnsearchedItems = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -123,29 +124,26 @@ function ImportSeriesFooter() {
|
|||
setAddSeriesOption(name as keyof AddSeriesOptions, value);
|
||||
|
||||
getSelectedIds().forEach((id) => {
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id,
|
||||
[name]: value,
|
||||
})
|
||||
);
|
||||
updateImportSeriesItem({
|
||||
id,
|
||||
[name]: value,
|
||||
});
|
||||
});
|
||||
},
|
||||
[getSelectedIds, dispatch]
|
||||
[getSelectedIds]
|
||||
);
|
||||
|
||||
const handleLookupPress = useCallback(() => {
|
||||
dispatch(lookupUnsearchedSeries());
|
||||
}, [dispatch]);
|
||||
startProcessing();
|
||||
}, []);
|
||||
|
||||
const handleCancelLookupPress = useCallback(() => {
|
||||
dispatch(cancelLookupSeries());
|
||||
}, [dispatch]);
|
||||
stopProcessing();
|
||||
}, []);
|
||||
|
||||
const handleImportPress = useCallback(() => {
|
||||
dispatch(importSeries({ ids: getSelectedIds() }));
|
||||
}, [getSelectedIds, dispatch]);
|
||||
importSeries(getSelectedIds());
|
||||
}, [importSeries, getSelectedIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMonitorMixed && monitor !== 'mixed') {
|
||||
|
|
@ -286,12 +284,12 @@ function ImportSeriesFooter() {
|
|||
title={translate('ImportErrors')}
|
||||
body={
|
||||
<ul>
|
||||
{Array.isArray(importError.responseJSON) ? (
|
||||
importError.responseJSON.map((error, index) => {
|
||||
{Array.isArray(importError.statusBody) ? (
|
||||
importError.statusBody.map((error, index) => {
|
||||
return <li key={index}>{error.errorMessage}</li>;
|
||||
})
|
||||
) : (
|
||||
<li>{JSON.stringify(importError.responseJSON)}</li>
|
||||
<li>{JSON.stringify(importError.statusBody)}</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,29 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useSelect } from 'App/Select/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import useExistingSeries from 'Series/useExistingSeries';
|
||||
import { setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import {
|
||||
ImportSeriesItem,
|
||||
UnamppedFolderItem,
|
||||
updateImportSeriesItem,
|
||||
useImportSeriesItem,
|
||||
} from './importSeriesStore';
|
||||
import ImportSeriesSelectSeries from './SelectSeries/ImportSeriesSelectSeries';
|
||||
import styles from './ImportSeriesRow.css';
|
||||
|
||||
function createItemSelector(id: string) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.importSeries.items,
|
||||
(items) => {
|
||||
return (
|
||||
items.find((item) => {
|
||||
return item.id === id;
|
||||
}) || ({} as ImportSeries)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface ImportSeriesRowProps {
|
||||
id: string;
|
||||
unmappedFolder: UnamppedFolderItem;
|
||||
}
|
||||
|
||||
function ImportSeriesRow({ id }: ImportSeriesRowProps) {
|
||||
const dispatch = useDispatch();
|
||||
function ImportSeriesRow({ unmappedFolder }: ImportSeriesRowProps) {
|
||||
const id = unmappedFolder.id;
|
||||
|
||||
const item = useImportSeriesItem(unmappedFolder.id);
|
||||
|
||||
const {
|
||||
relativePath,
|
||||
|
|
@ -42,24 +32,18 @@ function ImportSeriesRow({ id }: ImportSeriesRowProps) {
|
|||
seasonFolder,
|
||||
seriesType,
|
||||
selectedSeries,
|
||||
} = useSelector(createItemSelector(id));
|
||||
} = item ?? {};
|
||||
|
||||
const isExistingSeries = useExistingSeries(selectedSeries?.tvdbId);
|
||||
|
||||
const { getIsSelected, toggleSelected, toggleDisabled } =
|
||||
useSelect<ImportSeries>();
|
||||
useSelect<ImportSeriesItem>();
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id,
|
||||
[name]: value,
|
||||
})
|
||||
);
|
||||
updateImportSeriesItem({ id, [name]: value });
|
||||
},
|
||||
[id, dispatch]
|
||||
[id]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
|
|
@ -77,6 +61,10 @@ function ImportSeriesRow({ id }: ImportSeriesRowProps) {
|
|||
toggleDisabled(id, !selectedSeries || isExistingSeries);
|
||||
}, [id, selectedSeries, isExistingSeries, toggleDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
toggleSelected({ id, isSelected: !!selectedSeries, shiftKey: false });
|
||||
}, [id, selectedSeries, toggleSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualTableSelectCell<string>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,25 @@
|
|||
import React, { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { RefObject, useCallback, useRef } from 'react';
|
||||
import { FixedSizeList, ListChildComponentProps } from 'react-window';
|
||||
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
|
||||
import { useAppDimension } from 'App/appStore';
|
||||
import { useSelect } from 'App/Select/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import {
|
||||
queueLookupSeries,
|
||||
setImportSeriesValue,
|
||||
} from 'Store/Actions/importSeriesActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { UnmappedFolder } from 'typings/RootFolder';
|
||||
import ImportSeriesHeader from './ImportSeriesHeader';
|
||||
import ImportSeriesRow from './ImportSeriesRow';
|
||||
import {
|
||||
UnamppedFolderItem,
|
||||
useEnsureImportSeriesItems,
|
||||
} from './importSeriesStore';
|
||||
import styles from './ImportSeriesTable.css';
|
||||
|
||||
const ROW_HEIGHT = 52;
|
||||
|
||||
interface RowItemData {
|
||||
items: ImportSeries[];
|
||||
items: UnamppedFolderItem[];
|
||||
}
|
||||
|
||||
interface ImportSeriesTableProps {
|
||||
unmappedFolders: UnmappedFolder[];
|
||||
items: UnamppedFolderItem[];
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
|
|
@ -49,42 +41,17 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
|||
}}
|
||||
className={styles.row}
|
||||
>
|
||||
<ImportSeriesRow key={item.id} id={item.id} />
|
||||
<ImportSeriesRow key={item.id} unmappedFolder={item} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ImportSeriesTable({
|
||||
unmappedFolders,
|
||||
scrollerRef,
|
||||
}: ImportSeriesTableProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { monitor, qualityProfileId, seriesType, seasonFolder } =
|
||||
useAddSeriesOptions();
|
||||
|
||||
const items = useSelector((state: AppState) => state.importSeries.items);
|
||||
function ImportSeriesTable({ items, scrollerRef }: ImportSeriesTableProps) {
|
||||
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||
const { data: allSeries } = useSeries();
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
getIsSelected,
|
||||
toggleSelected,
|
||||
selectAll,
|
||||
unselectAll,
|
||||
} = useSelect();
|
||||
|
||||
const defaultValues = useRef({
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
});
|
||||
const { allSelected, allUnselected, selectAll, unselectAll, useHasItems } =
|
||||
useSelect();
|
||||
|
||||
const listRef = useRef<FixedSizeList<RowItemData>>(null);
|
||||
const initialUnmappedFolders = useRef(unmappedFolders);
|
||||
const previousItems = usePrevious(items);
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
|
|
@ -97,95 +64,11 @@ function ImportSeriesTable({
|
|||
[selectAll, unselectAll]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }: SelectStateInputProps<string>) => {
|
||||
toggleSelected({
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[toggleSelected]
|
||||
);
|
||||
const hasSelectItems = useHasItems();
|
||||
|
||||
// TODO: Check if this is still needed
|
||||
const handleRemoveSelectedStateItem = useCallback((_id: string) => {
|
||||
// selectDispatch({
|
||||
// type: 'removeItem',
|
||||
// id,
|
||||
// });
|
||||
}, []);
|
||||
useEnsureImportSeriesItems(items);
|
||||
|
||||
useEffect(() => {
|
||||
initialUnmappedFolders.current.forEach(({ name, path, relativePath }) => {
|
||||
dispatch(
|
||||
queueLookupSeries({
|
||||
name,
|
||||
path,
|
||||
relativePath,
|
||||
term: name,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id: name,
|
||||
...defaultValues.current,
|
||||
})
|
||||
);
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
previousItems?.forEach((prevItem) => {
|
||||
const { id } = prevItem;
|
||||
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
handleRemoveSelectedStateItem(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSeries = item.selectedSeries;
|
||||
const isSelected = getIsSelected(id);
|
||||
|
||||
const isExistingSeries =
|
||||
!!selectedSeries &&
|
||||
allSeries.some((s) => s.tvdbId === selectedSeries.tvdbId);
|
||||
|
||||
if (
|
||||
(!selectedSeries && prevItem.selectedSeries) ||
|
||||
(isExistingSeries && !prevItem.selectedSeries)
|
||||
) {
|
||||
handleSelectedChange({ id, value: false, shiftKey: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected && (!selectedSeries || isExistingSeries)) {
|
||||
handleSelectedChange({ id, value: false, shiftKey: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSeries && selectedSeries !== prevItem.selectedSeries) {
|
||||
handleSelectedChange({ id, value: true, shiftKey: false });
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [
|
||||
allSeries,
|
||||
items,
|
||||
previousItems,
|
||||
handleRemoveSelectedStateItem,
|
||||
handleSelectedChange,
|
||||
getIsSelected,
|
||||
]);
|
||||
|
||||
if (!items.length) {
|
||||
if (!items.length || !hasSelectItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,23 +7,27 @@ import {
|
|||
useFloating,
|
||||
useInteractions,
|
||||
} from '@floating-ui/react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import useImportSeriesItem from 'AddSeries/ImportSeries/Import/useImportSeriesItem';
|
||||
import AppState from 'App/State/AppState';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLookupSeries } from 'AddSeries/AddNewSeries/useAddSeries';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import useDebounce from 'Helpers/Hooks/useDebounce';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
queueLookupSeries,
|
||||
setImportSeriesValue,
|
||||
} from 'Store/Actions/importSeriesActions';
|
||||
import useExistingSeries from 'Series/useExistingSeries';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import {
|
||||
addToLookupQueue,
|
||||
removeFromLookupQueue,
|
||||
updateImportSeriesItem,
|
||||
useImportSeriesItem,
|
||||
useIsCurrentedItemQueued,
|
||||
useIsCurrentLookupQueueItem,
|
||||
} from '../importSeriesStore';
|
||||
import ImportSeriesSearchResult from './ImportSeriesSearchResult';
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSelectSeries.css';
|
||||
|
|
@ -37,28 +41,23 @@ function ImportSeriesSelectSeries({
|
|||
id,
|
||||
onInputChange,
|
||||
}: ImportSeriesSelectSeriesProps) {
|
||||
const dispatch = useDispatch();
|
||||
const isLookingUpSeries = useSelector(
|
||||
(state: AppState) => state.importSeries.isLookingUpSeries
|
||||
const importSeriesItem = useImportSeriesItem(id);
|
||||
const { selectedSeries, name } = importSeriesItem ?? {};
|
||||
const isExistingSeries = useExistingSeries(selectedSeries?.tvdbId);
|
||||
|
||||
const [term, setTerm] = useState(name);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const query = useDebounce(term, term ? 300 : 0);
|
||||
const isCurrentLookupQueueItem = useIsCurrentLookupQueueItem(id);
|
||||
const isQueued = useIsCurrentedItemQueued(id);
|
||||
|
||||
const { isFetching, isFetched, error, data, refetch } = useLookupSeries(
|
||||
query,
|
||||
isCurrentLookupQueueItem
|
||||
);
|
||||
|
||||
const {
|
||||
error,
|
||||
isFetching = true,
|
||||
isPopulated = false,
|
||||
items = [],
|
||||
isQueued = true,
|
||||
selectedSeries,
|
||||
isExistingSeries,
|
||||
term: itemTerm,
|
||||
} = useImportSeriesItem(id);
|
||||
|
||||
const seriesLookupTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const [term, setTerm] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const isLookingUpSeries = isFetching || isQueued;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
|
|
@ -66,48 +65,26 @@ function ImportSeriesSelectSeries({
|
|||
|
||||
const handleSearchInputChange = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
if (seriesLookupTimeout.current) {
|
||||
clearTimeout(seriesLookupTimeout.current);
|
||||
}
|
||||
|
||||
setTerm(value);
|
||||
|
||||
seriesLookupTimeout.current = setTimeout(() => {
|
||||
dispatch(
|
||||
queueLookupSeries({
|
||||
name: id,
|
||||
term: value,
|
||||
topOfQueue: true,
|
||||
})
|
||||
);
|
||||
}, 200);
|
||||
addToLookupQueue(id);
|
||||
},
|
||||
[id, dispatch]
|
||||
[id]
|
||||
);
|
||||
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
queueLookupSeries({
|
||||
name: id,
|
||||
term,
|
||||
topOfQueue: true,
|
||||
})
|
||||
);
|
||||
}, [id, term, dispatch]);
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
const handleSeriesSelect = useCallback(
|
||||
(tvdbId: number) => {
|
||||
setIsOpen(false);
|
||||
|
||||
const selectedSeries = items.find((item) => item.tvdbId === tvdbId)!;
|
||||
const selectedSeries = data.find((item) => item.tvdbId === tvdbId)!;
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id,
|
||||
selectedSeries,
|
||||
})
|
||||
);
|
||||
updateImportSeriesItem({
|
||||
id,
|
||||
selectedSeries,
|
||||
});
|
||||
|
||||
if (selectedSeries.seriesType !== 'standard') {
|
||||
onInputChange({
|
||||
|
|
@ -116,12 +93,24 @@ function ImportSeriesSelectSeries({
|
|||
});
|
||||
}
|
||||
},
|
||||
[id, items, dispatch, onInputChange]
|
||||
[id, data, onInputChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTerm(itemTerm);
|
||||
}, [itemTerm]);
|
||||
if (isFetched) {
|
||||
updateImportSeriesItem({
|
||||
id,
|
||||
hasSearched: isFetched,
|
||||
selectedSeries: data[0],
|
||||
});
|
||||
|
||||
removeFromLookupQueue(id);
|
||||
}
|
||||
}, [id, isFetched, data]);
|
||||
|
||||
useEffect(() => {
|
||||
setTerm(name);
|
||||
}, [name]);
|
||||
|
||||
const { refs, context, floatingStyles } = useFloating({
|
||||
middleware: [
|
||||
|
|
@ -148,11 +137,11 @@ function ImportSeriesSelectSeries({
|
|||
<>
|
||||
<div ref={refs.setReference} {...getReferenceProps()}>
|
||||
<Link className={styles.button} component="div" onPress={handlePress}>
|
||||
{isLookingUpSeries && isQueued && !isPopulated ? (
|
||||
{isLookingUpSeries && isQueued && !isFetched ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
|
||||
{isPopulated && selectedSeries && isExistingSeries ? (
|
||||
{isFetched && selectedSeries && isExistingSeries ? (
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
|
|
@ -160,7 +149,7 @@ function ImportSeriesSelectSeries({
|
|||
/>
|
||||
) : null}
|
||||
|
||||
{isPopulated && selectedSeries ? (
|
||||
{isFetched && selectedSeries ? (
|
||||
<ImportSeriesTitle
|
||||
title={selectedSeries.title}
|
||||
year={selectedSeries.year}
|
||||
|
|
@ -169,7 +158,7 @@ function ImportSeriesSelectSeries({
|
|||
/>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !selectedSeries ? (
|
||||
{isFetched && !selectedSeries ? (
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
|
|
@ -199,6 +188,7 @@ function ImportSeriesSelectSeries({
|
|||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{isOpen ? (
|
||||
<FloatingPortal id="portal-root">
|
||||
<div
|
||||
|
|
@ -233,7 +223,7 @@ function ImportSeriesSelectSeries({
|
|||
</div>
|
||||
|
||||
<div className={styles.results}>
|
||||
{items.map((item) => {
|
||||
{data.map((item) => {
|
||||
return (
|
||||
<ImportSeriesSearchResult
|
||||
key={item.tvdbId}
|
||||
|
|
|
|||
177
frontend/src/AddSeries/ImportSeries/Import/importSeriesStore.ts
Normal file
177
frontend/src/AddSeries/ImportSeries/Import/importSeriesStore.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { useEffect } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
|
||||
import { UnmappedFolder } from 'RootFolder/useRootFolders';
|
||||
import Series, { SeriesMonitor, SeriesType } from 'Series/Series';
|
||||
|
||||
export interface UnamppedFolderItem extends UnmappedFolder {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ImportSeriesItem {
|
||||
id: string;
|
||||
monitor: SeriesMonitor;
|
||||
path: string;
|
||||
qualityProfileId: number;
|
||||
relativePath: string;
|
||||
seasonFolder: boolean;
|
||||
selectedSeries?: Series;
|
||||
seriesType: SeriesType;
|
||||
name: string;
|
||||
hasSearched: boolean;
|
||||
}
|
||||
|
||||
interface ImportSeriesState {
|
||||
items: Record<string, ImportSeriesItem>;
|
||||
lookupQueue: string[];
|
||||
isProcessing: boolean;
|
||||
}
|
||||
|
||||
const defaultState: ImportSeriesState = {
|
||||
items: {},
|
||||
lookupQueue: [],
|
||||
isProcessing: false,
|
||||
};
|
||||
|
||||
const importSeriesStore = create<ImportSeriesState>()(() => defaultState);
|
||||
|
||||
export const useEnsureImportSeriesItems = (
|
||||
unmappedFolders: UnamppedFolderItem[]
|
||||
) => {
|
||||
const { monitor, qualityProfileId, seriesType, seasonFolder } =
|
||||
useAddSeriesOptions();
|
||||
|
||||
useEffect(() => {
|
||||
unmappedFolders.forEach((unmappedFolder) => {
|
||||
const existingItem =
|
||||
importSeriesStore.getState().items[unmappedFolder.id];
|
||||
|
||||
if (existingItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem: ImportSeriesItem = {
|
||||
...unmappedFolder,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
hasSearched: false,
|
||||
};
|
||||
|
||||
importSeriesStore.setState((state) => ({
|
||||
items: {
|
||||
...state.items,
|
||||
[unmappedFolder.id]: newItem,
|
||||
},
|
||||
}));
|
||||
});
|
||||
}, [unmappedFolders, monitor, qualityProfileId, seriesType, seasonFolder]);
|
||||
};
|
||||
|
||||
export const updateImportSeriesItem = (
|
||||
itemData: Partial<ImportSeriesItem> & Pick<ImportSeriesItem, 'id'>
|
||||
) => {
|
||||
console.info('\x1b[36m[MarkTest] updating item\x1b[0m', itemData);
|
||||
|
||||
importSeriesStore.setState((state) => {
|
||||
const existingItem = state.items[itemData.id];
|
||||
|
||||
if (existingItem) {
|
||||
return {
|
||||
items: {
|
||||
...state.items,
|
||||
[itemData.id]: {
|
||||
...existingItem,
|
||||
...itemData,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
export const removeImportSeriesItemByPath = (path: string) => {
|
||||
importSeriesStore.setState((state) => {
|
||||
const item = Object.values(state.items).find((i) => i.path === path);
|
||||
|
||||
if (!item) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const { [item.id]: removed, ...items } = state.items;
|
||||
|
||||
return { items };
|
||||
});
|
||||
};
|
||||
|
||||
export const clearImportSeries = () => {
|
||||
importSeriesStore.setState(defaultState);
|
||||
};
|
||||
|
||||
export const startProcessing = () => {
|
||||
importSeriesStore.setState((state) => {
|
||||
const items = Object.values(state.items).reduce<string[]>((acc, item) => {
|
||||
if (!item.hasSearched) {
|
||||
acc.push(item.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return { isProcessing: true, lookupQueue: items };
|
||||
});
|
||||
};
|
||||
|
||||
export const stopProcessing = () => {
|
||||
importSeriesStore.setState({ isProcessing: false, lookupQueue: [] });
|
||||
};
|
||||
|
||||
export const addToLookupQueue = (id: string) => {
|
||||
importSeriesStore.setState((state) => ({
|
||||
lookupQueue: [...state.lookupQueue, id],
|
||||
}));
|
||||
};
|
||||
|
||||
export const removeFromLookupQueue = (id: string) => {
|
||||
importSeriesStore.setState((state) => ({
|
||||
lookupQueue: state.lookupQueue.filter((queuedId) => queuedId !== id),
|
||||
}));
|
||||
};
|
||||
|
||||
export const useIsCurrentLookupQueueItem = (id: string) => {
|
||||
return importSeriesStore((state) => state.lookupQueue[0] === id);
|
||||
};
|
||||
|
||||
export const useIsCurrentedItemQueued = (id: string) => {
|
||||
return importSeriesStore((state) => state.lookupQueue.includes(id));
|
||||
};
|
||||
|
||||
export const useLookupQueueHasItems = () => {
|
||||
return importSeriesStore((state) => state.lookupQueue.length > 0);
|
||||
};
|
||||
|
||||
export const useImportSeriesItem = (id: string) => {
|
||||
return importSeriesStore((state) => state.items[id]);
|
||||
};
|
||||
|
||||
export const useImportSeriesItems = () => {
|
||||
return importSeriesStore(useShallow((state) => Object.values(state.items)));
|
||||
};
|
||||
|
||||
export const getImportSeriesItems = (ids: string[]) => {
|
||||
const state = importSeriesStore.getState();
|
||||
|
||||
return ids.reduce<ImportSeriesItem[]>((acc, id) => {
|
||||
const item = state.items[id];
|
||||
|
||||
if (item != null) {
|
||||
acc.push(item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import Series from 'Series/Series';
|
||||
import {
|
||||
getImportSeriesItems,
|
||||
removeImportSeriesItemByPath,
|
||||
} from './importSeriesStore';
|
||||
|
||||
export const useImportSeries = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { isPending, error, mutate } = useApiMutation<Series[], Series[]>({
|
||||
path: '/series/import',
|
||||
method: 'POST',
|
||||
mutationOptions: {
|
||||
onSuccess: (data, newSeries) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/rootFolder'] });
|
||||
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
||||
if (!oldSeries) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return [...oldSeries, ...data];
|
||||
});
|
||||
|
||||
newSeries.forEach((series) => {
|
||||
removeImportSeriesItemByPath(series.path);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const importSeries = useCallback(
|
||||
(ids: string[]) => {
|
||||
const items = getImportSeriesItems(ids);
|
||||
const addedIds: string[] = [];
|
||||
|
||||
const allNewSeries = ids.reduce<Series[]>((acc, id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
const selectedSeries = item?.selectedSeries;
|
||||
|
||||
// Make sure we have a selected series and the same series hasn't been added yet.
|
||||
if (
|
||||
selectedSeries &&
|
||||
!acc.some((a) => a.tvdbId === selectedSeries.tvdbId)
|
||||
) {
|
||||
const newSeries: Series = {
|
||||
...selectedSeries,
|
||||
monitored: true,
|
||||
monitorNewItems: 'all',
|
||||
qualityProfileId: item.qualityProfileId,
|
||||
path: item.path,
|
||||
seriesType: item.seriesType,
|
||||
seasonFolder: item.seasonFolder,
|
||||
addOptions: {
|
||||
monitor: item.monitor,
|
||||
searchForMissingEpisodes: false,
|
||||
searchForCutoffUnmetEpisodes: false,
|
||||
},
|
||||
tags: [],
|
||||
};
|
||||
|
||||
newSeries.path = item.path;
|
||||
|
||||
addedIds.push(id);
|
||||
acc.push(newSeries);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (allNewSeries.length > 0) {
|
||||
mutate(allNewSeries);
|
||||
}
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return {
|
||||
isImporting: isPending,
|
||||
importError: error,
|
||||
importSeries,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
||||
import useSeries from 'Series/useSeries';
|
||||
|
||||
function useImportSeriesItem(id: string) {
|
||||
const { data: series = [] } = useSeries();
|
||||
const importSeries = useSelector((state: AppState) => state.importSeries);
|
||||
|
||||
return useMemo(() => {
|
||||
const item =
|
||||
importSeries.items.find((item) => {
|
||||
return item.id === id;
|
||||
}) ?? ({} as ImportSeries);
|
||||
|
||||
const selectedSeries = item && item.selectedSeries;
|
||||
const isExistingSeries =
|
||||
!!selectedSeries &&
|
||||
series.some((s) => {
|
||||
return s.tvdbId === selectedSeries.tvdbId;
|
||||
});
|
||||
|
||||
return {
|
||||
...item,
|
||||
isExistingSeries,
|
||||
};
|
||||
}, [id, importSeries.items, series]);
|
||||
}
|
||||
|
||||
export default useImportSeriesItem;
|
||||
|
|
@ -214,6 +214,15 @@ export default function useSelectStore<T extends SelectStoreModel<Id>>(
|
|||
);
|
||||
};
|
||||
|
||||
const useHasItems = () => {
|
||||
return useStore(
|
||||
store.current,
|
||||
useShallow((state) => {
|
||||
return state.itemState.size > 0;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.current.subscribe((state) => {
|
||||
const itemState = state.itemState;
|
||||
|
|
@ -231,7 +240,9 @@ export default function useSelectStore<T extends SelectStoreModel<Id>>(
|
|||
return acc;
|
||||
},
|
||||
{
|
||||
allSelected: itemState.size > 0,
|
||||
allSelected:
|
||||
itemState.size > 0 &&
|
||||
itemState.values().some((i) => i.isSelected),
|
||||
allUnselected: true,
|
||||
anySelected: false,
|
||||
selectedCount: 0,
|
||||
|
|
@ -296,6 +307,7 @@ export default function useSelectStore<T extends SelectStoreModel<Id>>(
|
|||
toggleDisabled,
|
||||
toggleSelected,
|
||||
unselectAll,
|
||||
useHasItems,
|
||||
useIsSelected,
|
||||
useSelectedIds,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import CaptchaAppState from './CaptchaAppState';
|
||||
import ImportSeriesAppState from './ImportSeriesAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
|
||||
interface AppState {
|
||||
captcha: CaptchaAppState;
|
||||
importSeries: ImportSeriesAppState;
|
||||
settings: SettingsAppState;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import Series, { SeriesMonitor, SeriesType } from 'Series/Series';
|
||||
import { Error } from './AppSectionState';
|
||||
|
||||
export interface ImportSeries {
|
||||
id: string;
|
||||
error?: Error;
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
isQueued: boolean;
|
||||
items: Series[];
|
||||
monitor: SeriesMonitor;
|
||||
path: string;
|
||||
qualityProfileId: number;
|
||||
relativePath: string;
|
||||
seasonFolder: boolean;
|
||||
selectedSeries?: Series;
|
||||
seriesType: SeriesType;
|
||||
term: string;
|
||||
}
|
||||
|
||||
interface ImportSeriesAppState {
|
||||
isLookingUpSeries: false;
|
||||
isImporting: false;
|
||||
isImported: false;
|
||||
importError: Error | null;
|
||||
items: ImportSeries[];
|
||||
}
|
||||
|
||||
export default ImportSeriesAppState;
|
||||
|
|
@ -165,7 +165,6 @@ export const useReprocessInteractiveImportItems = () => {
|
|||
})[0];
|
||||
|
||||
if (!currentData) {
|
||||
console.info('\x1b[36m[MarkTest] no data\x1b[0m');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,14 +36,18 @@ const useRootFolders = () => {
|
|||
};
|
||||
|
||||
export const useRootFolder = (id: number, timeout: boolean) => {
|
||||
const result = useApiQuery<RootFolder[]>({
|
||||
const result = useApiQuery<RootFolder>({
|
||||
path: `/rootFolder/${id}`,
|
||||
queryParams: { timeout },
|
||||
queryOptions: {
|
||||
// Disable refetch on window focus to prevent refetching when the user switch tabs
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? DEFAULT_ROOT_FOLDERS,
|
||||
data: result.data,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,336 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getNewSeries from 'Utilities/Series/getNewSeries';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import { removeItem, set, updateItem } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'importSeries';
|
||||
let concurrentLookups = 0;
|
||||
let abortCurrentLookup = null;
|
||||
const queue = [];
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isLookingUpSeries: false,
|
||||
isImporting: false,
|
||||
isImported: false,
|
||||
importError: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const QUEUE_LOOKUP_SERIES = 'importSeries/queueLookupSeries';
|
||||
export const START_LOOKUP_SERIES = 'importSeries/startLookupSeries';
|
||||
export const CANCEL_LOOKUP_SERIES = 'importSeries/cancelLookupSeries';
|
||||
export const LOOKUP_UNSEARCHED_SERIES = 'importSeries/lookupUnsearchedSeries';
|
||||
export const CLEAR_IMPORT_SERIES = 'importSeries/clearImportSeries';
|
||||
export const SET_IMPORT_SERIES_VALUE = 'importSeries/setImportSeriesValue';
|
||||
export const IMPORT_SERIES = 'importSeries/importSeries';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const queueLookupSeries = createThunk(QUEUE_LOOKUP_SERIES);
|
||||
export const startLookupSeries = createThunk(START_LOOKUP_SERIES);
|
||||
export const importSeries = createThunk(IMPORT_SERIES);
|
||||
export const lookupUnsearchedSeries = createThunk(LOOKUP_UNSEARCHED_SERIES);
|
||||
export const clearImportSeries = createAction(CLEAR_IMPORT_SERIES);
|
||||
export const cancelLookupSeries = createAction(CANCEL_LOOKUP_SERIES);
|
||||
|
||||
export const setImportSeriesValue = createAction(SET_IMPORT_SERIES_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[QUEUE_LOOKUP_SERIES]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
name,
|
||||
path,
|
||||
relativePath,
|
||||
term,
|
||||
topOfQueue = false
|
||||
} = payload;
|
||||
|
||||
const state = getState().importSeries;
|
||||
const item = _.find(state.items, { id: name }) || {
|
||||
id: name,
|
||||
term,
|
||||
path,
|
||||
relativePath,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
...item,
|
||||
term,
|
||||
isQueued: true,
|
||||
items: []
|
||||
}));
|
||||
|
||||
const itemIndex = queue.indexOf(item.id);
|
||||
|
||||
if (itemIndex >= 0) {
|
||||
queue.splice(itemIndex, 1);
|
||||
}
|
||||
|
||||
if (topOfQueue) {
|
||||
queue.unshift(item.id);
|
||||
} else {
|
||||
queue.push(item.id);
|
||||
}
|
||||
|
||||
if (term && term.length > 2) {
|
||||
dispatch(startLookupSeries({ start: true }));
|
||||
}
|
||||
},
|
||||
|
||||
[START_LOOKUP_SERIES]: function(getState, payload, dispatch) {
|
||||
if (concurrentLookups >= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState().importSeries;
|
||||
|
||||
const {
|
||||
isLookingUpSeries,
|
||||
items
|
||||
} = state;
|
||||
|
||||
const queueId = queue[0];
|
||||
|
||||
if (payload.start && !isLookingUpSeries) {
|
||||
dispatch(set({ section, isLookingUpSeries: true }));
|
||||
} else if (!isLookingUpSeries) {
|
||||
return;
|
||||
} else if (!queueId) {
|
||||
dispatch(set({ section, isLookingUpSeries: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
concurrentLookups++;
|
||||
queue.splice(0, 1);
|
||||
|
||||
const queued = items.find((i) => i.id === queueId);
|
||||
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
isFetching: true
|
||||
}));
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest({
|
||||
url: '/series/lookup',
|
||||
data: {
|
||||
term: queued.term
|
||||
}
|
||||
});
|
||||
|
||||
abortCurrentLookup = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
const selectedSeries = queued.selectedSeries || data[0];
|
||||
|
||||
const itemProps = {
|
||||
section,
|
||||
id: queued.id,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null,
|
||||
items: data,
|
||||
isQueued: false,
|
||||
selectedSeries,
|
||||
updateOnly: true
|
||||
};
|
||||
|
||||
if (selectedSeries && selectedSeries.seriesType !== seriesTypes.STANDARD) {
|
||||
itemProps.seriesType = selectedSeries.seriesType;
|
||||
}
|
||||
|
||||
dispatch(updateItem(itemProps));
|
||||
});
|
||||
|
||||
request.fail((xhr) => {
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr,
|
||||
isQueued: false,
|
||||
updateOnly: true
|
||||
}));
|
||||
});
|
||||
|
||||
request.always(() => {
|
||||
concurrentLookups--;
|
||||
|
||||
dispatch(startLookupSeries());
|
||||
});
|
||||
},
|
||||
|
||||
[LOOKUP_UNSEARCHED_SERIES]: function(getState, payload, dispatch) {
|
||||
const state = getState().importSeries;
|
||||
|
||||
if (state.isLookingUpSeries) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.items.forEach((item) => {
|
||||
const id = item.id;
|
||||
|
||||
if (
|
||||
!item.isPopulated &&
|
||||
!queue.includes(id)
|
||||
) {
|
||||
queue.push(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (queue.length) {
|
||||
dispatch(startLookupSeries({ start: true }));
|
||||
}
|
||||
},
|
||||
|
||||
[IMPORT_SERIES]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isImporting: true }));
|
||||
|
||||
const ids = payload.ids;
|
||||
const items = getState().importSeries.items;
|
||||
const addedIds = [];
|
||||
|
||||
const allNewSeries = ids.reduce((acc, id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
const selectedSeries = item.selectedSeries;
|
||||
|
||||
// Make sure we have a selected series and
|
||||
// the same series hasn't been added yet.
|
||||
if (selectedSeries && !acc.some((a) => a.tvdbId === selectedSeries.tvdbId)) {
|
||||
const newSeries = getNewSeries(_.cloneDeep(selectedSeries), item);
|
||||
newSeries.path = item.path;
|
||||
|
||||
addedIds.push(id);
|
||||
acc.push(newSeries);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/series/import',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(allNewSeries)
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
set({
|
||||
section,
|
||||
isImporting: false,
|
||||
isImported: true,
|
||||
importError: null
|
||||
}),
|
||||
|
||||
...data.map((series) => updateItem({ section: 'series', ...series })),
|
||||
|
||||
...addedIds.map((id) => removeItem({ section, id }))
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(batchActions([
|
||||
set({
|
||||
section,
|
||||
isImporting: false,
|
||||
isImported: true,
|
||||
importError: xhr
|
||||
}),
|
||||
|
||||
...addedIds.map((id) => updateItem({
|
||||
section,
|
||||
id
|
||||
}))
|
||||
]));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CANCEL_LOOKUP_SERIES]: function(state) {
|
||||
queue.splice(0, queue.length);
|
||||
|
||||
const items = state.items.map((item) => {
|
||||
if (item.isQueued) {
|
||||
return {
|
||||
...item,
|
||||
isQueued: false
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isLookingUpSeries: false,
|
||||
items
|
||||
});
|
||||
},
|
||||
|
||||
[CLEAR_IMPORT_SERIES]: function(state) {
|
||||
if (abortCurrentLookup) {
|
||||
abortCurrentLookup();
|
||||
|
||||
abortCurrentLookup = null;
|
||||
}
|
||||
|
||||
queue.splice(0, queue.length);
|
||||
|
||||
return Object.assign({}, state, defaultState);
|
||||
},
|
||||
|
||||
[SET_IMPORT_SERIES_VALUE]: function(state, { payload }) {
|
||||
const newState = getSectionState(state, section);
|
||||
const items = newState.items;
|
||||
const index = items.findIndex((item) => item.id === payload.id);
|
||||
|
||||
newState.items = [...items];
|
||||
|
||||
if (index >= 0) {
|
||||
const item = items[index];
|
||||
|
||||
newState.items.splice(index, 1, { ...item, ...payload });
|
||||
} else {
|
||||
newState.items.push({ ...payload });
|
||||
}
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import * as captcha from './captchaActions';
|
||||
import * as importSeries from './importSeriesActions';
|
||||
import * as settings from './settingsActions';
|
||||
|
||||
export default [
|
||||
captcha,
|
||||
importSeries,
|
||||
settings
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue