mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
22 Commits
v5.23.1.99
...
v5.23.3.99
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0b93a03fd | ||
|
|
f7f5837d49 | ||
|
|
c3ee8b3c90 | ||
|
|
4de78e3bab | ||
|
|
426538c8af | ||
|
|
c82404c75b | ||
|
|
9bee9841c1 | ||
|
|
010959d915 | ||
|
|
a600728916 | ||
|
|
bbfb8c7cc2 | ||
|
|
32418ea521 | ||
|
|
2c5c99e9b7 | ||
|
|
a5e5a63e45 | ||
|
|
31b44d2c2e | ||
|
|
da8e8a12de | ||
|
|
6506c97ce1 | ||
|
|
5303a1992c | ||
|
|
042308c319 | ||
|
|
2e97e09f44 | ||
|
|
ccfb9c0dad | ||
|
|
b655d97e9e | ||
|
|
3afcb91db6 |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.23.1'
|
||||
majorVersion: '5.23.3'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
@@ -481,6 +481,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: ne(variables['testName'], 'freebsd-x64')
|
||||
|
||||
- job: Unit_Docker
|
||||
displayName: Unit Docker
|
||||
@@ -540,7 +541,8 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
failTaskOnMissingResultsFile: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres14
|
||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||
dependsOn: Prepare
|
||||
@@ -596,6 +598,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres15
|
||||
displayName: Unit Native LinuxCore with Postgres15 Database
|
||||
@@ -652,6 +655,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
@@ -734,6 +738,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres14
|
||||
@@ -796,6 +801,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
|
||||
@@ -859,6 +865,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_FreeBSD
|
||||
@@ -905,6 +912,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'FreeBSD Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: false
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_Docker
|
||||
@@ -974,6 +982,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
failTaskOnMissingResultsFile: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- stage: Automation
|
||||
@@ -1055,6 +1064,7 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(osName) Automation Tests'
|
||||
failTaskOnFailedTests: $(failBuild)
|
||||
failTaskOnMissingResultsFile: $(failBuild)
|
||||
displayName: Publish Test Results
|
||||
|
||||
- stage: Analyze
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
input1 = """Prometheus.Special.Edition.Fan Edit.2012..BRRip.x264.AAC-m2g
|
||||
Star Wars Episode IV - A New Hope (Despecialized) 1999.mkv
|
||||
Prometheus.(Special.Edition.Remastered).2012.[Bluray-1080p].mkv
|
||||
Prometheus Extended 2012
|
||||
Prometheus Extended Directors Cut Fan Edit 2012
|
||||
Prometheus Director's Cut 2012
|
||||
Prometheus Directors Cut 2012
|
||||
Prometheus.(Extended.Theatrical.Version.IMAX).BluRay.1080p.2012.asdf
|
||||
2001 A Space Odyssey Director's Cut (1968).mkv
|
||||
2001: A Space Odyssey (Extended Directors Cut FanEdit) Bluray 1080p 1968
|
||||
A Fake Movie 2035 Directors 2012.mkv
|
||||
Blade Runner Director's Cut 2049.mkv
|
||||
Prometheus 50th Anniversary Edition 2012.mkv
|
||||
Movie 2in1 2012.mkv
|
||||
Movie IMAX 2012.mkv"""
|
||||
|
||||
output1 = """Special.Edition.Fan Edit BRRip.x264.AAC-m2g
|
||||
Despecialized mkv
|
||||
Special.Edition.Remastered Bluray-1080p].mkv
|
||||
Extended mkv
|
||||
Extended Directors Cut Fan Edit mkv
|
||||
Director's Cut mkv
|
||||
Directors Cut mkv
|
||||
Extended.Theatrical.Version.IMAX asdf
|
||||
Director's Cut mkv
|
||||
Extended Directors Cut FanEdit mkv
|
||||
Directors mkv
|
||||
Director's Cut mkv
|
||||
50th Anniversary Edition mkv
|
||||
2in1 mkv
|
||||
IMAX mkv"""
|
||||
|
||||
inputs = input1.split("\n")
|
||||
outputs = output1.split("\n")
|
||||
real_o = []
|
||||
for output in outputs:
|
||||
real_o.append(output.split(" ")[0].replace(".", " ").strip())
|
||||
|
||||
count = 0
|
||||
|
||||
for inp in inputs:
|
||||
o = real_o[count]
|
||||
print "[TestCase(\"{0}\", \"{1}\")]".format(inp, o)
|
||||
count += 1
|
||||
@@ -176,7 +176,7 @@ module.exports = (env) => {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: '3.41'
|
||||
corejs: '3.42'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -32,8 +32,8 @@ import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||
import CutoffUnmet from 'Wanted/CutoffUnmet/CutoffUnmet';
|
||||
import Missing from 'Wanted/Missing/Missing';
|
||||
|
||||
function RedirectWithUrlBase() {
|
||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||
@@ -89,9 +89,9 @@ function AppRoutes() {
|
||||
Wanted
|
||||
*/}
|
||||
|
||||
<Route path="/wanted/missing" component={MissingConnector} />
|
||||
<Route path="/wanted/missing" component={Missing} />
|
||||
|
||||
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
|
||||
<Route path="/wanted/cutoffunmet" component={CutoffUnmet} />
|
||||
|
||||
{/*
|
||||
Settings
|
||||
|
||||
@@ -24,6 +24,7 @@ import RootFolderAppState from './RootFolderAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
import WantedAppState from './WantedAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
id: string;
|
||||
@@ -47,14 +48,14 @@ export interface PropertyFilter {
|
||||
export interface Filter {
|
||||
key: string;
|
||||
label: string | (() => string);
|
||||
filers: PropertyFilter[];
|
||||
filters: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface CustomFilter {
|
||||
id: number;
|
||||
type: string;
|
||||
label: string;
|
||||
filers: PropertyFilter[];
|
||||
filters: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
@@ -105,6 +106,7 @@ interface AppState {
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
wanted: WantedAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
|
||||
29
frontend/src/App/State/WantedAppState.ts
Normal file
29
frontend/src/App/State/WantedAppState.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Movie from 'Movie/Movie';
|
||||
|
||||
interface WantedMovie extends Movie {
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
interface WantedCutoffUnmetAppState
|
||||
extends AppSectionState<WantedMovie>,
|
||||
AppSectionFilterState<WantedMovie>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {}
|
||||
|
||||
interface WantedMissingAppState
|
||||
extends AppSectionState<WantedMovie>,
|
||||
AppSectionFilterState<WantedMovie>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {}
|
||||
|
||||
interface WantedAppState {
|
||||
cutoffUnmet: WantedCutoffUnmetAppState;
|
||||
missing: WantedMissingAppState;
|
||||
}
|
||||
|
||||
export default WantedAppState;
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
RenderSuggestion,
|
||||
SuggestionsFetchRequestedParams,
|
||||
} from 'react-autosuggest';
|
||||
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import AutoSuggestInput from '../AutoSuggestInput';
|
||||
|
||||
@@ -13,10 +13,10 @@ import React, {
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
|
||||
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Movie from 'Movie/Movie';
|
||||
|
||||
@@ -129,7 +129,6 @@ function VirtualTable<T extends ModelBase>({
|
||||
>
|
||||
{header}
|
||||
|
||||
{/* @ts-expect-error - ref type is incompatible */}
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
{...otherProps}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { debounce, DebouncedFunc, DebounceSettings } from 'lodash';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default function useDebouncedCallback<T extends (...args: any) => any>(
|
||||
callback: T,
|
||||
delay: number,
|
||||
options?: DebounceSettings
|
||||
): DebouncedFunc<T> {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useCallback(debounce(callback, delay, options), [
|
||||
callback,
|
||||
delay,
|
||||
options,
|
||||
]);
|
||||
}
|
||||
@@ -968,7 +968,6 @@ function MovieDetails({ movieId }: MovieDetailsProps) {
|
||||
<MovieInteractiveSearchModal
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
movieId={id}
|
||||
movieTitle={title}
|
||||
onModalClose={handleInteractiveSearchModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
@@ -6,6 +6,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { MovieEntity } from 'Movie/useMovie';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -14,12 +15,10 @@ import styles from './MovieSearchCell.css';
|
||||
|
||||
interface MovieSearchCellProps {
|
||||
movieId: number;
|
||||
movieTitle: string;
|
||||
movieEntity?: MovieEntity;
|
||||
}
|
||||
|
||||
function MovieSearchCell(props: MovieSearchCellProps) {
|
||||
const { movieId, movieTitle } = props;
|
||||
|
||||
function MovieSearchCell({ movieId }: MovieSearchCellProps) {
|
||||
const executingCommands = useSelector(createExecutingCommandsSelector());
|
||||
const isSearching = executingCommands.some(({ name, body }) => {
|
||||
const { movieIds = [] } = body;
|
||||
@@ -61,7 +60,6 @@ function MovieSearchCell(props: MovieSearchCellProps) {
|
||||
<MovieInteractiveSearchModal
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
movieId={movieId}
|
||||
movieTitle={movieTitle}
|
||||
onModalClose={setInteractiveSearchModalClosed}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
@@ -17,9 +17,11 @@ interface MovieInteractiveSearchModalProps
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function MovieInteractiveSearchModal(props: MovieInteractiveSearchModalProps) {
|
||||
const { isOpen, movieId, movieTitle, onModalClose } = props;
|
||||
|
||||
function MovieInteractiveSearchModal({
|
||||
isOpen,
|
||||
movieId,
|
||||
onModalClose,
|
||||
}: MovieInteractiveSearchModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
@@ -41,7 +43,6 @@ function MovieInteractiveSearchModal(props: MovieInteractiveSearchModalProps) {
|
||||
>
|
||||
<MovieInteractiveSearchModalContent
|
||||
movieId={movieId}
|
||||
movieTitle={movieTitle}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -7,6 +7,8 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
|
||||
import Movie from 'Movie/Movie';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { clearMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { clearMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import {
|
||||
@@ -17,17 +19,17 @@ import translate from 'Utilities/String/translate';
|
||||
|
||||
export interface MovieInteractiveSearchModalContentProps {
|
||||
movieId: number;
|
||||
movieTitle?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function MovieInteractiveSearchModalContent({
|
||||
movieId,
|
||||
movieTitle,
|
||||
onModalClose,
|
||||
}: MovieInteractiveSearchModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { title, year } = useMovie(movieId) as Movie;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(cancelFetchReleases());
|
||||
@@ -38,6 +40,8 @@ function MovieInteractiveSearchModalContent({
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const movieTitle = `${title}${year > 0 ? ` (${year})` : ''}`;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
||||
@@ -249,6 +249,9 @@ function Naming() {
|
||||
translate('MovieFolderFormatHelpText'),
|
||||
...movieFolderFormatHelpTexts,
|
||||
]}
|
||||
helpTextWarning={translate(
|
||||
'MovieFolderFormatHelpTextDeprecatedWarning'
|
||||
)}
|
||||
errors={[
|
||||
...movieFolderFormatErrors,
|
||||
...settings.movieFolderFormat.errors,
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
export default function getFilterValue(filters, filterKey, filterValueKey, defaultValue) {
|
||||
import { Filter } from 'App/State/AppState';
|
||||
|
||||
export default function getFilterValue<T>(
|
||||
filters: Filter[],
|
||||
filterKey: string | number,
|
||||
filterValueKey: string,
|
||||
defaultValue: T
|
||||
) {
|
||||
const filter = filters.find((f) => f.key === filterKey);
|
||||
|
||||
if (!filter) {
|
||||
@@ -1,292 +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 ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
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 PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
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 getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import CutoffUnmetRow from './CutoffUnmetRow';
|
||||
|
||||
function getMonitoredValue(props) {
|
||||
const {
|
||||
filters,
|
||||
selectedFilterKey
|
||||
} = props;
|
||||
|
||||
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||
}
|
||||
|
||||
class CutoffUnmet extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmSearchAllCutoffUnmetModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFilterMenuItemPress = (filterKey, filterValue) => {
|
||||
this.props.onFilterSelect(filterKey, filterValue);
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onSearchSelectedPress = () => {
|
||||
const selected = this.getSelectedIds();
|
||||
|
||||
this.props.onSearchSelectedPress(selected);
|
||||
};
|
||||
|
||||
onToggleSelectedPress = () => {
|
||||
const movieIds = this.getSelectedIds();
|
||||
|
||||
this.props.batchToggleCutoffUnmetMovies({
|
||||
movieIds,
|
||||
monitored: !getMonitoredValue(this.props)
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetPress = () => {
|
||||
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true });
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetConfirmed = () => {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
onSearchAllCutoffUnmetPress
|
||||
} = this.props;
|
||||
|
||||
// TODO: Custom filters will need to check whether there is a monitored
|
||||
// filter once implemented.
|
||||
|
||||
onSearchAllCutoffUnmetPress(selectedFilterKey === 'monitored');
|
||||
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmSearchAllCutoffUnmetModalClose = () => {
|
||||
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
columns,
|
||||
totalRecords,
|
||||
isSearchingForCutoffUnmetMovies,
|
||||
isSaving,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmSearchAllCutoffUnmetModalOpen
|
||||
} = this.state;
|
||||
|
||||
const itemsSelected = !!this.getSelectedIds().length;
|
||||
const isShowingMonitored = getMonitoredValue(this.props);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('CutoffUnmet')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={isSearchingForCutoffUnmetMovies}
|
||||
isSpinning={isSearchingForCutoffUnmetMovies}
|
||||
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllCutoffUnmetPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||
iconName={icons.MONITORED}
|
||||
isDisabled={!itemsSelected}
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onToggleSelectedPress}
|
||||
/>
|
||||
</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={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('CutoffUnmetLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('CutoffUnmetNoItems')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<CutoffUnmetRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('SearchForCutoffUnmetMovies')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('SearchForCutoffUnmetMoviesConfirmationCount', { totalRecords })}
|
||||
</div>
|
||||
<div>
|
||||
{translate('MassSearchCancelWarning')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Search')}
|
||||
onConfirm={this.onSearchAllCutoffUnmetConfirmed}
|
||||
onCancel={this.onConfirmSearchAllCutoffUnmetModalClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CutoffUnmet.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isSearchingForCutoffUnmetMovies: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||
batchToggleCutoffUnmetMovies: PropTypes.func.isRequired,
|
||||
onSearchAllCutoffUnmetPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CutoffUnmet;
|
||||
358
frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx
Normal file
358
frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState, { Filter } from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
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 PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
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 useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
batchToggleCutoffUnmetMovies,
|
||||
clearCutoffUnmet,
|
||||
fetchCutoffUnmet,
|
||||
gotoCutoffUnmetPage,
|
||||
setCutoffUnmetFilter,
|
||||
setCutoffUnmetSort,
|
||||
setCutoffUnmetTableOption,
|
||||
} from 'Store/Actions/wantedActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import CutoffUnmetRow from './CutoffUnmetRow';
|
||||
|
||||
function getMonitoredValue(
|
||||
filters: Filter[],
|
||||
selectedFilterKey: string
|
||||
): boolean {
|
||||
return !!getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||
}
|
||||
|
||||
function CutoffUnmet() {
|
||||
const dispatch = useDispatch();
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords = 0,
|
||||
} = useSelector((state: AppState) => state.wanted.cutoffUnmet);
|
||||
|
||||
const isSearchingForAllMovies = useSelector(
|
||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH)
|
||||
);
|
||||
const isSearchingForSelectedMovies = useSelector(
|
||||
createCommandExecutingSelector(commandNames.MOVIE_SEARCH)
|
||||
);
|
||||
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
|
||||
const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoCutoffUnmetPage,
|
||||
});
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const isSaving = useMemo(() => {
|
||||
return items.filter((m) => m.isSaving).length > 1;
|
||||
}, [items]);
|
||||
|
||||
const itemsSelected = !!selectedIds.length;
|
||||
const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey);
|
||||
const isSearchingForMovies =
|
||||
isSearchingForAllMovies || isSearchingForSelectedMovies;
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSearchSelectedPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selectedIds,
|
||||
commandFinished: () => {
|
||||
dispatch(fetchCutoffUnmet());
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [selectedIds, dispatch]);
|
||||
|
||||
const handleSearchAllPress = useCallback(() => {
|
||||
setIsConfirmSearchAllModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmSearchAllCutoffUnmetModalClose = useCallback(() => {
|
||||
setIsConfirmSearchAllModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSearchAllCutoffUnmetConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
||||
commandFinished: () => {
|
||||
dispatch(fetchCutoffUnmet());
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
setIsConfirmSearchAllModalOpen(false);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleToggleSelectedPress = useCallback(() => {
|
||||
dispatch(
|
||||
batchToggleCutoffUnmetMovies({
|
||||
movieIds: selectedIds,
|
||||
monitored: !isShowingMonitored,
|
||||
})
|
||||
);
|
||||
}, [isShowingMonitored, selectedIds, dispatch]);
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(filterKey: number | string) => {
|
||||
dispatch(setCutoffUnmetFilter({ selectedFilterKey: filterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string) => {
|
||||
dispatch(setCutoffUnmetSort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setCutoffUnmetTableOption(payload));
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoCutoffUnmetPage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchCutoffUnmet());
|
||||
} else {
|
||||
dispatch(gotoCutoffUnmetPage({ page: 1 }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(clearCutoffUnmet());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchCutoffUnmet());
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate, [
|
||||
'movieUpdated',
|
||||
'movieFileUpdated',
|
||||
'movieFileDeleted',
|
||||
]);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('CutoffUnmet')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={
|
||||
itemsSelected
|
||||
? translate('SearchSelected')
|
||||
: translate('SearchAll')
|
||||
}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={isSearchingForMovies}
|
||||
isSpinning={isSearchingForMovies}
|
||||
onPress={
|
||||
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
|
||||
}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={
|
||||
isShowingMonitored
|
||||
? translate('UnmonitorSelected')
|
||||
: translate('MonitorSelected')
|
||||
}
|
||||
iconName={icons.MONITORED}
|
||||
isDisabled={!itemsSelected}
|
||||
isSpinning={isSaving}
|
||||
onPress={handleToggleSelectedPress}
|
||||
/>
|
||||
</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={[]}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('CutoffUnmetLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('CutoffUnmetNoItems')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !!items.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<CutoffUnmetRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmSearchAllModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('SearchForCutoffUnmetMovies')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('SearchForCutoffUnmetMoviesConfirmationCount', {
|
||||
totalRecords,
|
||||
})}
|
||||
</div>
|
||||
<div>{translate('MassSearchCancelWarning')}</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Search')}
|
||||
onConfirm={handleSearchAllCutoffUnmetConfirmed}
|
||||
onCancel={handleConfirmSearchAllCutoffUnmetModalClose}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CutoffUnmet;
|
||||
@@ -1,188 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import CutoffUnmet from './CutoffUnmet';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.wanted.cutoffUnmet,
|
||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
|
||||
createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
|
||||
(cutoffUnmet, isSearchingForCutoffUnmetMovies, isSearchingForSelectedCutoffUnmetMovies) => {
|
||||
return {
|
||||
isSearchingForCutoffUnmetMovies: isSearchingForCutoffUnmetMovies || isSearchingForSelectedCutoffUnmetMovies,
|
||||
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
|
||||
...cutoffUnmet
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...wantedActions,
|
||||
executeCommand,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails,
|
||||
fetchMovieFiles,
|
||||
clearMovieFiles
|
||||
};
|
||||
|
||||
class CutoffUnmetConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchCutoffUnmet,
|
||||
gotoCutoffUnmetFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate, ['movieUpdated', 'movieFileUpdated', 'movieFileDeleted']);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchCutoffUnmet();
|
||||
} else {
|
||||
gotoCutoffUnmetFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||
const movieFileIds = selectUniqueIds(this.props.items, 'movieFileId');
|
||||
|
||||
this.props.fetchQueueDetails({ movieIds });
|
||||
|
||||
if (movieFileIds.length) {
|
||||
this.props.fetchMovieFiles({ movieFileIds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearCutoffUnmet();
|
||||
this.props.clearQueueDetails();
|
||||
this.props.clearMovieFiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchCutoffUnmet();
|
||||
};
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoCutoffUnmetFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoCutoffUnmetPreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoCutoffUnmetNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoCutoffUnmetLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoCutoffUnmetPage({ page });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setCutoffUnmetSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setCutoffUnmetFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setCutoffUnmetTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoCutoffUnmetFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetPress = (monitored) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
||||
monitored,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CutoffUnmet
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||
onToggleSelectedPress={this.onToggleSelectedPress}
|
||||
onSearchAllCutoffUnmetPress={this.onSearchAllCutoffUnmetPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CutoffUnmetConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchCutoffUnmet: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetFirstPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetNextPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetLastPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetPage: PropTypes.func.isRequired,
|
||||
setCutoffUnmetSort: PropTypes.func.isRequired,
|
||||
setCutoffUnmetFilter: PropTypes.func.isRequired,
|
||||
setCutoffUnmetTableOption: PropTypes.func.isRequired,
|
||||
clearCutoffUnmet: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
clearMovieFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector)
|
||||
);
|
||||
@@ -1,172 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import movieEntities from 'Movie/movieEntities';
|
||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
|
||||
import styles from './CutoffUnmetRow.css';
|
||||
|
||||
function CutoffUnmetRow(props) {
|
||||
const {
|
||||
id,
|
||||
movieFileId,
|
||||
year,
|
||||
title,
|
||||
titleSlug,
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
physicalRelease,
|
||||
lastSearchTime,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={titleSlug}
|
||||
title={title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.year') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{year}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.inCinemas') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={inCinemas}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.digitalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={digitalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.physicalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={physicalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.languages}
|
||||
>
|
||||
<MovieFileLanguages
|
||||
movieFileId={movieFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<MovieStatus
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<MovieSearchCell
|
||||
key={name}
|
||||
movieId={id}
|
||||
movieTitle={title}
|
||||
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
CutoffUnmetRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movieFileId: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
inCinemas: PropTypes.string,
|
||||
digitalRelease: PropTypes.string,
|
||||
physicalRelease: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CutoffUnmetRow;
|
||||
150
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.tsx
Normal file
150
frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import styles from './CutoffUnmetRow.css';
|
||||
|
||||
interface CutoffUnmetRowProps {
|
||||
id: number;
|
||||
movieFileId?: number;
|
||||
inCinemas?: string;
|
||||
digitalRelease?: string;
|
||||
physicalRelease?: string;
|
||||
lastSearchTime?: string;
|
||||
title: string;
|
||||
year: number;
|
||||
titleSlug: string;
|
||||
isSelected?: boolean;
|
||||
columns: Column[];
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function CutoffUnmetRow({
|
||||
id,
|
||||
movieFileId,
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
physicalRelease,
|
||||
lastSearchTime,
|
||||
title,
|
||||
year,
|
||||
titleSlug,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
}: CutoffUnmetRowProps) {
|
||||
if (!movieFileId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink titleSlug={titleSlug} title={title} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.year') {
|
||||
return <TableRowCell key={name}>{year}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.inCinemas') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={inCinemas}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.digitalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={digitalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.physicalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={physicalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.languages}>
|
||||
<MovieFileLanguages movieFileId={movieFileId} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.status}>
|
||||
<MovieStatus
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity="wanted.cutoffUnmet"
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<MovieSearchCell
|
||||
key={name}
|
||||
movieId={id}
|
||||
movieEntity="wanted.cutoffUnmet"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default CutoffUnmetRow;
|
||||
@@ -1,312 +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 ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
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 PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
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 InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import MissingRow from './MissingRow';
|
||||
|
||||
function getMonitoredValue(props) {
|
||||
const {
|
||||
filters,
|
||||
selectedFilterKey
|
||||
} = props;
|
||||
|
||||
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||
}
|
||||
|
||||
class Missing extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmSearchAllMissingModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onSearchSelectedPress = () => {
|
||||
const selected = this.getSelectedIds();
|
||||
|
||||
this.props.onSearchSelectedPress(selected);
|
||||
};
|
||||
|
||||
onToggleSelectedPress = () => {
|
||||
const movieIds = this.getSelectedIds();
|
||||
|
||||
this.props.batchToggleMissingMovies({
|
||||
movieIds,
|
||||
monitored: !getMonitoredValue(this.props)
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllMissingPress = () => {
|
||||
this.setState({ isConfirmSearchAllMissingModalOpen: true });
|
||||
};
|
||||
|
||||
onSearchAllMissingConfirmed = () => {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
onSearchAllMissingPress
|
||||
} = this.props;
|
||||
|
||||
// TODO: Custom filters will need to check whether there is a monitored
|
||||
// filter once implemented.
|
||||
|
||||
onSearchAllMissingPress(selectedFilterKey === 'monitored');
|
||||
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmSearchAllMissingModalClose = () => {
|
||||
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
columns,
|
||||
totalRecords,
|
||||
isSearchingForMissingMovies,
|
||||
isSaving,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmSearchAllMissingModalOpen,
|
||||
isInteractiveImportModalOpen
|
||||
} = this.state;
|
||||
|
||||
const itemsSelected = !!this.getSelectedIds().length;
|
||||
const isShowingMonitored = getMonitoredValue(this.props);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Missing')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isSpinning={isSearchingForMissingMovies}
|
||||
isDisabled={isSearchingForMissingMovies}
|
||||
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllMissingPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||
iconName={icons.MONITORED}
|
||||
isDisabled={!itemsSelected}
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onToggleSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManualImport')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
</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={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('MissingLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('MissingNoItems')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<MissingRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmSearchAllMissingModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('SearchForAllMissingMovies')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('SearchForAllMissingMoviesConfirmationCount', { totalRecords })}
|
||||
</div>
|
||||
<div>
|
||||
{translate('MassSearchCancelWarning')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Search')}
|
||||
onConfirm={this.onSearchAllMissingConfirmed}
|
||||
onCancel={this.onConfirmSearchAllMissingModalClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Missing.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isSearchingForMissingMovies: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||
batchToggleMissingMovies: PropTypes.func.isRequired,
|
||||
onSearchAllMissingPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Missing;
|
||||
383
frontend/src/Wanted/Missing/Missing.tsx
Normal file
383
frontend/src/Wanted/Missing/Missing.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState, { Filter } from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
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 PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
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 useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
batchToggleMissingMovies,
|
||||
clearMissing,
|
||||
fetchMissing,
|
||||
gotoMissingPage,
|
||||
setMissingFilter,
|
||||
setMissingSort,
|
||||
setMissingTableOption,
|
||||
} from 'Store/Actions/wantedActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import MissingRow from './MissingRow';
|
||||
|
||||
function getMonitoredValue(
|
||||
filters: Filter[],
|
||||
selectedFilterKey: string
|
||||
): boolean {
|
||||
return !!getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||
}
|
||||
|
||||
function Missing() {
|
||||
const dispatch = useDispatch();
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords = 0,
|
||||
} = useSelector((state: AppState) => state.wanted.missing);
|
||||
|
||||
const isSearchingForAllMovies = useSelector(
|
||||
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH)
|
||||
);
|
||||
const isSearchingForSelectedMovies = useSelector(
|
||||
createCommandExecutingSelector(commandNames.MOVIE_SEARCH)
|
||||
);
|
||||
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
|
||||
const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoMissingPage,
|
||||
});
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
const isSaving = useMemo(() => {
|
||||
return items.filter((m) => m.isSaving).length > 1;
|
||||
}, [items]);
|
||||
|
||||
const itemsSelected = !!selectedIds.length;
|
||||
const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey);
|
||||
const isSearchingForMovies =
|
||||
isSearchingForAllMovies || isSearchingForSelectedMovies;
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const handleSearchSelectedPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selectedIds,
|
||||
commandFinished: () => {
|
||||
dispatch(fetchMissing());
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [selectedIds, dispatch]);
|
||||
|
||||
const handleSearchAllPress = useCallback(() => {
|
||||
setIsConfirmSearchAllModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmSearchAllMissingModalClose = useCallback(() => {
|
||||
setIsConfirmSearchAllModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSearchAllMissingConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.MISSING_MOVIES_SEARCH,
|
||||
commandFinished: () => {
|
||||
dispatch(fetchMissing());
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
setIsConfirmSearchAllModalOpen(false);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleToggleSelectedPress = useCallback(() => {
|
||||
dispatch(
|
||||
batchToggleMissingMovies({
|
||||
movieIds: selectedIds,
|
||||
monitored: !isShowingMonitored,
|
||||
})
|
||||
);
|
||||
}, [isShowingMonitored, selectedIds, dispatch]);
|
||||
|
||||
const handleInteractiveImportPress = useCallback(() => {
|
||||
setIsInteractiveImportModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleInteractiveImportModalClose = useCallback(() => {
|
||||
setIsInteractiveImportModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(filterKey: number | string) => {
|
||||
dispatch(setMissingFilter({ selectedFilterKey: filterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string) => {
|
||||
dispatch(setMissingSort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setMissingTableOption(payload));
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoMissingPage({ page: 1 }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchMissing());
|
||||
} else {
|
||||
dispatch(gotoMissingPage({ page: 1 }));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(clearMissing());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchMissing());
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate, [
|
||||
'movieUpdated',
|
||||
'movieFileUpdated',
|
||||
'movieFileDeleted',
|
||||
]);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Missing')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={
|
||||
itemsSelected
|
||||
? translate('SearchSelected')
|
||||
: translate('SearchAll')
|
||||
}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={isSearchingForMovies}
|
||||
isSpinning={isSearchingForMovies}
|
||||
onPress={
|
||||
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
|
||||
}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={
|
||||
isShowingMonitored
|
||||
? translate('UnmonitorSelected')
|
||||
: translate('MonitorSelected')
|
||||
}
|
||||
iconName={icons.MONITORED}
|
||||
isDisabled={!itemsSelected}
|
||||
isSpinning={isSaving}
|
||||
onPress={handleToggleSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManualImport')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
onPress={handleInteractiveImportPress}
|
||||
/>
|
||||
</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={[]}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('MissingLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !items.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('MissingNoItems')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !!items.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onTableOptionChange={handleTableOptionChange}
|
||||
onSelectAllChange={handleSelectAllChange}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<MissingRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmSearchAllModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('SearchForAllMissingMovies')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('SearchForAllMissingMoviesConfirmationCount', {
|
||||
totalRecords,
|
||||
})}
|
||||
</div>
|
||||
<div>{translate('MassSearchCancelWarning')}</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Search')}
|
||||
onConfirm={handleSearchAllMissingConfirmed}
|
||||
onCancel={handleConfirmSearchAllMissingModalClose}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
onModalClose={handleInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Missing;
|
||||
@@ -1,176 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Missing from './Missing';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.wanted.missing,
|
||||
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
|
||||
createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
|
||||
(missing, isSearchingForMissingMovies, isSearchingForSelectedMissingMovies) => {
|
||||
return {
|
||||
isSearchingForMissingMovies: isSearchingForMissingMovies || isSearchingForSelectedMissingMovies,
|
||||
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
|
||||
...missing
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...wantedActions,
|
||||
executeCommand,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails
|
||||
};
|
||||
|
||||
class MissingConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchMissing,
|
||||
gotoMissingFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchMissing();
|
||||
} else {
|
||||
gotoMissingFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||
this.props.fetchQueueDetails({ movieIds });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearMissing();
|
||||
this.props.clearQueueDetails();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchMissing();
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoMissingFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoMissingPreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoMissingNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoMissingLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoMissingPage({ page });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setMissingSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setMissingFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setMissingTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoMissingFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllMissingPress = (monitored) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MISSING_MOVIES_SEARCH,
|
||||
monitored,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Missing
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||
onSearchAllMissingPress={this.onSearchAllMissingPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MissingConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchMissing: PropTypes.func.isRequired,
|
||||
gotoMissingFirstPage: PropTypes.func.isRequired,
|
||||
gotoMissingPreviousPage: PropTypes.func.isRequired,
|
||||
gotoMissingNextPage: PropTypes.func.isRequired,
|
||||
gotoMissingLastPage: PropTypes.func.isRequired,
|
||||
gotoMissingPage: PropTypes.func.isRequired,
|
||||
setMissingSort: PropTypes.func.isRequired,
|
||||
setMissingFilter: PropTypes.func.isRequired,
|
||||
setMissingTableOption: PropTypes.func.isRequired,
|
||||
clearMissing: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(MissingConnector)
|
||||
);
|
||||
@@ -1,162 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import movieEntities from 'Movie/movieEntities';
|
||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import styles from './MissingRow.css';
|
||||
|
||||
function MissingRow(props) {
|
||||
const {
|
||||
id,
|
||||
movieFileId,
|
||||
year,
|
||||
title,
|
||||
titleSlug,
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
physicalRelease,
|
||||
lastSearchTime,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={titleSlug}
|
||||
title={title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.year') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{year}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.inCinemas') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={inCinemas}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.digitalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={digitalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.physicalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={physicalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<MovieStatus
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity={movieEntities.WANTED_MISSING}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<MovieSearchCell
|
||||
key={name}
|
||||
movieId={id}
|
||||
movieTitle={title}
|
||||
movieEntity={movieEntities.WANTED_MISSING}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
MissingRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movieFileId: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
inCinemas: PropTypes.string,
|
||||
digitalRelease: PropTypes.string,
|
||||
physicalRelease: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MissingRow;
|
||||
141
frontend/src/Wanted/Missing/MissingRow.tsx
Normal file
141
frontend/src/Wanted/Missing/MissingRow.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React from 'react';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import styles from './MissingRow.css';
|
||||
|
||||
interface MissingRowProps {
|
||||
id: number;
|
||||
movieFileId?: number;
|
||||
inCinemas?: string;
|
||||
digitalRelease?: string;
|
||||
physicalRelease?: string;
|
||||
lastSearchTime?: string;
|
||||
title: string;
|
||||
year: number;
|
||||
titleSlug: string;
|
||||
isSelected?: boolean;
|
||||
columns: Column[];
|
||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function MissingRow({
|
||||
id,
|
||||
movieFileId,
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
physicalRelease,
|
||||
lastSearchTime,
|
||||
title,
|
||||
year,
|
||||
titleSlug,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
}: MissingRowProps) {
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink titleSlug={titleSlug} title={title} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.year') {
|
||||
return <TableRowCell key={name}>{year}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.inCinemas') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={inCinemas}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.digitalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={digitalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.physicalRelease') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={physicalRelease}
|
||||
timeForToday={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.status}>
|
||||
<MovieStatus
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity="wanted.missing"
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<MovieSearchCell
|
||||
key={name}
|
||||
movieId={id}
|
||||
movieEntity="wanted.missing"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default MissingRow;
|
||||
41
package.json
41
package.json
@@ -22,11 +22,11 @@
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.7.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||
"@fortawesome/fontawesome-free": "6.7.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
@@ -34,14 +34,14 @@
|
||||
"@sentry/integrations": "7.119.1",
|
||||
"@tanstack/react-query": "5.74.3",
|
||||
"@types/node": "20.16.11",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react": "18.3.21",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"classnames": "2.5.1",
|
||||
"connected-react-router": "6.9.3",
|
||||
"copy-to-clipboard": "3.3.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.1.6",
|
||||
"fuse.js": "7.0.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"history": "4.10.1",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.7.1",
|
||||
@@ -65,7 +65,7 @@
|
||||
"react-dom": "18.3.1",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-lazyload": "3.2.1",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.7",
|
||||
"react-redux": "7.2.4",
|
||||
@@ -74,9 +74,9 @@
|
||||
"react-slider": "1.1.4",
|
||||
"react-tabs": "4.3.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-use-measure": "2.1.1",
|
||||
"react-virtualized": "9.21.1",
|
||||
"react-window": "1.8.10",
|
||||
"react-use-measure": "2.1.7",
|
||||
"react-virtualized": "9.22.6",
|
||||
"react-window": "1.8.11",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
@@ -85,16 +85,17 @@
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"swiper": "8.3.2",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.2",
|
||||
"use-debounce": "10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/eslint-parser": "7.25.9",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.9",
|
||||
"@babel/core": "7.27.1",
|
||||
"@babel/eslint-parser": "7.27.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.27.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/mousetrap": "1.6.15",
|
||||
"@types/react-autosuggest": "10.1.11",
|
||||
@@ -103,7 +104,7 @@
|
||||
"@types/react-lazyload": "3.2.3",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.19.0",
|
||||
"@types/react-virtualized": "9.22.0",
|
||||
"@types/react-virtualized": "9.22.2",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/redux-actions": "2.6.5",
|
||||
"@types/webpack-livereload-plugin": "2.3.6",
|
||||
@@ -113,7 +114,7 @@
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.41.0",
|
||||
"core-js": "3.42.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.1",
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
|
||||
first.Guid.Should().Be("PassThePopcorn-452135");
|
||||
first.Title.Should().Be("The.Night.Of.S01.BluRay.AAC2.0.x264-DEPTH");
|
||||
first.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
first.DownloadUrl.Should().Be("https://passthepopcorn.me/torrents.php?action=download&id=452135&authkey=00000000000000000000000000000000&torrent_pass=00000000000000000000000000000000");
|
||||
first.DownloadUrl.Should().Be("https://passthepopcorn.me/torrents.php?action=download&id=452135");
|
||||
first.InfoUrl.Should().Be("https://passthepopcorn.me/torrents.php?id=148131&torrentid=452135");
|
||||
|
||||
first.PublishDate.Should().Be(DateTime.Parse("2016-10-18T23:40:59+0000").ToUniversalTime());
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace NzbDrone.Core.Test.Languages
|
||||
new object[] { 51, Language.Afrikaans },
|
||||
new object[] { 52, Language.Marathi },
|
||||
new object[] { 53, Language.Tagalog },
|
||||
new object[] { 54, Language.Urdu }
|
||||
new object[] { 54, Language.Urdu },
|
||||
new object[] { 55, Language.Romansh }
|
||||
};
|
||||
|
||||
public static object[] ToIntCases =
|
||||
@@ -127,7 +128,8 @@ namespace NzbDrone.Core.Test.Languages
|
||||
new object[] { Language.Afrikaans, 51 },
|
||||
new object[] { Language.Marathi, 52 },
|
||||
new object[] { Language.Tagalog, 53 },
|
||||
new object[] { Language.Urdu, 54 }
|
||||
new object[] { Language.Urdu, 54 },
|
||||
new object[] { Language.Romansh, 55 }
|
||||
};
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -400,6 +400,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
[TestCase("rum", "RO")]
|
||||
[TestCase("per", "FA")]
|
||||
[TestCase("ger", "DE")]
|
||||
[TestCase("gsw", "DE")]
|
||||
[TestCase("cze", "CS")]
|
||||
[TestCase("ice", "IS")]
|
||||
[TestCase("dut", "NL")]
|
||||
|
||||
@@ -89,5 +89,14 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
var result = IsoLanguages.Find(isoCode);
|
||||
result.Language.Should().Be(Language.Urdu);
|
||||
}
|
||||
|
||||
[TestCase("rm")]
|
||||
[TestCase("roh")]
|
||||
[TestCase("rm-CH")]
|
||||
public void should_return_romansh(string isoCode)
|
||||
{
|
||||
var result = IsoLanguages.Find(isoCode);
|
||||
result.Language.Should().Be(Language.Romansh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Title.2016.Hun/Ger/Ita.AAC.1080p.WebDL.x264-TKP21")]
|
||||
[TestCase("Movie.Title.2016.1080p.10Bit.HEVC.WEBRip.HIN-ENG-GER.DD5.1.H.265")]
|
||||
[TestCase("Movie.Title.2016.HU-IT-DE.AAC.1080p.WebDL.x264")]
|
||||
[TestCase("Movie.Title.2016.SwissGerman.WEB-DL.h264-RlsGrp")]
|
||||
public void should_parse_language_german(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||
@@ -503,6 +504,15 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
result.Should().Contain(Language.Urdu);
|
||||
}
|
||||
|
||||
[TestCase("The.Movie.Name.2016.Romansh.WEB-DL.h264-RlsGrp")]
|
||||
[TestCase("The.Movie.Name.2016.Rumantsch.WEB.DL.h264-RlsGrp")]
|
||||
[TestCase("The Movie Name 2016 Romansch WEB DL h264-RlsGrp")]
|
||||
public void should_parse_language_romansh(string postTitle)
|
||||
{
|
||||
var result = LanguageParser.ParseLanguages(postTitle);
|
||||
result.Should().Contain(Language.Romansh);
|
||||
}
|
||||
|
||||
[TestCase("Movie.Title.en.sub")]
|
||||
[TestCase("Movie Title.eng.sub")]
|
||||
[TestCase("Movie.Title.eng.forced.sub")]
|
||||
|
||||
45
src/NzbDrone.Core/HealthCheck/Checks/NamingConfigCheck.cs
Normal file
45
src/NzbDrone.Core/HealthCheck/Checks/NamingConfigCheck.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Organizer;
|
||||
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ModelEvent<NamingConfig>))]
|
||||
public class NamingConfigCheck : HealthCheckBase, IProvideHealthCheck
|
||||
{
|
||||
private readonly INamingConfigService _namingConfigService;
|
||||
|
||||
public NamingConfigCheck(INamingConfigService namingConfigService, ILocalizationService localizationService)
|
||||
: base(localizationService)
|
||||
{
|
||||
_namingConfigService = namingConfigService;
|
||||
}
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var namingConfig = _namingConfigService.GetConfig();
|
||||
|
||||
if (namingConfig.MovieFolderFormat.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var match = FileNameValidation.DeprecatedMovieFolderTokensRegex.Matches(namingConfig.MovieFolderFormat);
|
||||
|
||||
if (match.Any())
|
||||
{
|
||||
return new HealthCheck(
|
||||
GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
_localizationService.GetLocalizedString(
|
||||
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "tokens", string.Join(", ", match.Select(c => c.Value).ToArray()) },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,8 @@ namespace NzbDrone.Core.ImportLists.TMDb
|
||||
[FieldOption(Hint = "Tagalog")]
|
||||
tl,
|
||||
[FieldOption(Hint = "Urdu")]
|
||||
ur
|
||||
ur,
|
||||
[FieldOption(Hint = "Raeto-Romance")]
|
||||
rm
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,15 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
{
|
||||
return new PassThePopcornParser(Settings, _logger);
|
||||
}
|
||||
|
||||
public override HttpRequest GetDownloadRequest(string link)
|
||||
{
|
||||
var request = new HttpRequest(link);
|
||||
|
||||
request.Headers.Set("ApiUser", Settings.APIUser);
|
||||
request.Headers.Set("ApiKey", Settings.APIKey);
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
public class PassThePopcornResponse
|
||||
{
|
||||
public string TotalResults { get; set; }
|
||||
public List<PassThePopcornMovie> Movies { get; set; }
|
||||
public IReadOnlyCollection<PassThePopcornMovie> Movies { get; set; }
|
||||
public string Page { get; set; }
|
||||
public string AuthKey { get; set; }
|
||||
public string PassKey { get; set; }
|
||||
}
|
||||
|
||||
public class PassThePopcornMovie
|
||||
@@ -18,9 +16,9 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
public string Title { get; set; }
|
||||
public string Year { get; set; }
|
||||
public string Cover { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public IReadOnlyCollection<string> Tags { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public List<PassThePopcornTorrent> Torrents { get; set; }
|
||||
public IReadOnlyCollection<PassThePopcornTorrent> Torrents { get; set; }
|
||||
}
|
||||
|
||||
public class PassThePopcornTorrent
|
||||
|
||||
@@ -75,7 +75,6 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
flags |= IndexerFlags.G_Scene;
|
||||
}
|
||||
|
||||
// Only add approved torrents
|
||||
try
|
||||
{
|
||||
torrentInfos.Add(new PassThePopcornInfo
|
||||
@@ -83,7 +82,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
Guid = $"PassThePopcorn-{id}",
|
||||
Title = torrent.ReleaseName,
|
||||
Size = long.Parse(torrent.Size),
|
||||
DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey),
|
||||
DownloadUrl = GetDownloadUrl(id),
|
||||
InfoUrl = GetInfoUrl(result.GroupId, id),
|
||||
Seeders = int.Parse(torrent.Seeders),
|
||||
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
|
||||
@@ -118,16 +117,12 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
torrentInfos;
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private string GetDownloadUrl(int torrentId, string authKey, string passKey)
|
||||
private string GetDownloadUrl(int torrentId)
|
||||
{
|
||||
var url = new HttpUri(_settings.BaseUrl)
|
||||
.CombinePath("/torrents.php")
|
||||
.AddQueryParam("action", "download")
|
||||
.AddQueryParam("id", torrentId)
|
||||
.AddQueryParam("authkey", authKey)
|
||||
.AddQueryParam("torrent_pass", passKey);
|
||||
.AddQueryParam("id", torrentId);
|
||||
|
||||
return url.FullUri;
|
||||
}
|
||||
@@ -141,5 +136,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
|
||||
return url.FullUri;
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ namespace NzbDrone.Core.Languages
|
||||
public static Language Marathi => new Language(52, "Marathi");
|
||||
public static Language Tagalog => new Language(53, "Tagalog");
|
||||
public static Language Urdu => new Language(54, "Urdu");
|
||||
public static Language Romansh => new Language(55, "Romansh");
|
||||
public static Language Any => new Language(-1, "Any");
|
||||
public static Language Original => new Language(-2, "Original");
|
||||
|
||||
@@ -189,6 +190,7 @@ namespace NzbDrone.Core.Languages
|
||||
Marathi,
|
||||
Tagalog,
|
||||
Urdu,
|
||||
Romansh,
|
||||
Any,
|
||||
Original
|
||||
};
|
||||
|
||||
@@ -1471,7 +1471,7 @@
|
||||
"NotificationsEmailSettingsServerHelpText": "Nom d'amfitrió o IP del servidor de correu electrònic",
|
||||
"NotificationsJoinValidationInvalidDeviceId": "Els ID del dispositiu no són vàlids.",
|
||||
"NotificationsSignalSettingsGroupIdPhoneNumber": "ID del grup / número de telèfon",
|
||||
"ParseModalUnableToParse": "No s'ha pogut analitzar el títol proporcionat. Torna-ho a provar.",
|
||||
"ParseModalUnableToParse": "No s'ha pogut analitzar el títol proporcionat. Torneu-ho a provar.",
|
||||
"ReleaseSource": "Origen del llançament",
|
||||
"RemotePathMappingsInfo": "Les mapes de camins remots poques vegades són necessaris, si {appName} i el vostre client de baixada són al mateix sistema, és millor que coincideixin amb els vostres camins. Per a més informació, vés al [wiki]({wikiLink}).",
|
||||
"RemoveQueueItem": "Elimina - {sourceTitle}",
|
||||
@@ -2014,5 +2014,9 @@
|
||||
"MovieEditRootFolderHelpText": "Moure pel·lícules a la mateixa carpeta arrel es pot utilitzar per a canviar de nom carpetes de pel·lícules per a fer coincidir títols actualitzats o format de noms",
|
||||
"UpdateMoviePath": "Actualitzar ruta de pel·lícula",
|
||||
"ImportListsRadarrSettingsApiKeyHelpText": "Clau API de la instància de {appName} des de la qual importar (Radarr 3.0 o superior)",
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "URL, incloent-hi port, de la instància de {appName} des de la qual importar (Radarr 3.0 o superior)"
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "URL, incloent-hi port, de la instància de {appName} des de la qual importar (Radarr 3.0 o superior)",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Inclou el cartell",
|
||||
"MovieFolderFormatHelpTextDeprecatedWarning": "Els tokens associats amb les propietats dels arxius de pel·lícula han quedat obsolets i no seran suportats en futures versions majors.",
|
||||
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "El format de carpeta de pel·lícules no ha de contenir tokens obsolets relacionats amb els arxius: {tokens}",
|
||||
"NotificationsAppriseSettingsIncludePosterHelpText": "Inclou el pòster al missatge"
|
||||
}
|
||||
|
||||
@@ -1323,5 +1323,20 @@
|
||||
"IndexerSettingsApiPathHelpText": "Cesta k api, obvykle {url}",
|
||||
"IndexerSettingsApiUrl": "URL API",
|
||||
"IndexerSettingsApiUser": "Uživatel API",
|
||||
"IndexerSettingsCategories": "Kategorie"
|
||||
"IndexerSettingsCategories": "Kategorie",
|
||||
"DownloadClientFloodSettingsPostImportTags": "Tagy po importu",
|
||||
"DownloadClientDownloadStationValidationSharedFolderMissing": "Sdílená složka neexistuje",
|
||||
"DownloadClientFreeboxNotLoggedIn": "Nepřihlášeno",
|
||||
"DownloadClientFreeboxUnableToReachFreebox": "Nelze se připojit k Freebox API. Zkontrolujte nastavení 'Host', 'Port' nebo 'Použít SSL'. (Chyba: {exceptionMessage})",
|
||||
"DownloadClientFloodSettingsStartOnAdd": "Spustit po přidání",
|
||||
"DownloadClientFloodSettingsPostImportTagsHelpText": "Přiřadí tagy po dokončení importu.",
|
||||
"DownloadClientFreeboxApiError": "Chyba z Freebox API: {errorDescription}",
|
||||
"DownloadClientDownloadStationValidationApiVersion": "Verze API Download Station není podporována. Požadovaná verze je alespoň {requiredVersion}. Podporovaný rozsah je od {minVersion} do {maxVersion}",
|
||||
"DownloadClientDownloadStationValidationFolderMissing": "Složka neexistuje",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestination": "Chybí výchozí cílová složka",
|
||||
"DownloadClientFloodSettingsRemovalInfo": "{appName} se postará o automatické mazání torrentů podle aktuálních kritérií seedování v Nastavení -> Indexery",
|
||||
"DownloadClientFreeboxAuthenticationError": "Přihlášení k Freebox API se nezdařilo. Důvod: {errorDescription}",
|
||||
"DownloadClientDownloadStationValidationFolderMissingDetail": "Složka '{downloadDir}' neexistuje, musí být vytvořená ručně ve Sdílené složce '{sharedFolder}'.",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Přihlaste se do vaší DiskStation jako {username} a ručně to nastavte v nastavení DownloadStation pod BT/HTTP/FTP/NZB -> Umístění.",
|
||||
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "DiskStation nemá sdílenou složku s názvem '{sharedFolder}', jste si jisti, že jste ji zadali správně?"
|
||||
}
|
||||
|
||||
@@ -1134,6 +1134,7 @@
|
||||
"MovieFilesTotaling": "Movie Files Totaling",
|
||||
"MovieFolderFormat": "Movie Folder Format",
|
||||
"MovieFolderFormatHelpText": "Used when adding a new movie or moving movies via the movie editor",
|
||||
"MovieFolderFormatHelpTextDeprecatedWarning": "Tokens associated with movie file properties have been deprecated and will no longer be supported in future major versions.",
|
||||
"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.",
|
||||
"MovieGrabbedTooltip": "Movie grabbed from {indexer} and sent to {downloadClient}",
|
||||
@@ -1173,6 +1174,7 @@
|
||||
"MustNotContainHelpText": "The release will be rejected if it contains one or more of terms (case insensitive)",
|
||||
"MyComputer": "My Computer",
|
||||
"Name": "Name",
|
||||
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "Movie Folder Format must not contain deprecated file related tokens: {tokens}",
|
||||
"NamingSettings": "Naming Settings",
|
||||
"NamingSettingsLoadError": "Unable to load Naming settings",
|
||||
"Negate": "Negate",
|
||||
@@ -1221,6 +1223,8 @@
|
||||
"NotificationTriggersHelpText": "Select which events should trigger this notification",
|
||||
"NotificationsAppriseSettingsConfigurationKey": "Apprise Configuration Key",
|
||||
"NotificationsAppriseSettingsConfigurationKeyHelpText": "Configuration Key for the Persistent Storage Solution. Leave empty if Stateless URLs is used.",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Include Poster",
|
||||
"NotificationsAppriseSettingsIncludePosterHelpText": "Include poster in message",
|
||||
"NotificationsAppriseSettingsNotificationType": "Apprise Notification Type",
|
||||
"NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth Password",
|
||||
"NotificationsAppriseSettingsServerUrl": "Apprise Server URL",
|
||||
|
||||
@@ -2014,5 +2014,9 @@
|
||||
"ImportListsRadarrSettingsTagsHelpText": "Etiquetas de la instancia de la fuente de la que importar",
|
||||
"ImportListsRadarrSettingsApiKeyHelpText": "Clave API de la instancia de {appName} desde la que importar (Radarr 3.0 o superior)",
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "URL, incluyendo puerto, de la instancia de {appName} desde la que importar (Radarr 3.0 o superior)",
|
||||
"EditMovieCollectionModalHeader": "Editar - {title}"
|
||||
"EditMovieCollectionModalHeader": "Editar - {title}",
|
||||
"NotificationsAppriseSettingsIncludePosterHelpText": "Incluir el póster en el mensaje",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Incluir póster",
|
||||
"MovieFolderFormatHelpTextDeprecatedWarning": "Los tokens asociados con las propiedades de los archivos de película han quedado obsoletos y no serán soportados en futuras versiones mayores.",
|
||||
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "El formato de carpeta de películas no debe contener tokens obsoletos relacionados con los archivos: {tokens}"
|
||||
}
|
||||
|
||||
@@ -828,7 +828,7 @@
|
||||
"Status": "Tila",
|
||||
"Studio": "Studio",
|
||||
"Style": "Ulkoasu",
|
||||
"AddNewMovieRootFolderHelpText": "Alikansio '{0}' luodaan automaattisesti",
|
||||
"AddNewMovieRootFolderHelpText": "Alikansio \"{folder}\" luodaan automaattisesti.",
|
||||
"Sunday": "Sunnuntai",
|
||||
"Table": "Taulukko",
|
||||
"TableOptions": "Taulukkonäkymän asetukset",
|
||||
@@ -1289,7 +1289,7 @@
|
||||
"ReleaseProfiles": "Julkaisuprofiilit",
|
||||
"UnknownEventTooltip": "Tuntematon tapahtuma",
|
||||
"DeleteSelectedMovieFilesHelpText": "Haluatko varmasti poistaa valitut elokuvatiedostot?",
|
||||
"ApplyTagsHelpTextHowToApplyMovies": "Tunnisteiden käyttö valituille elokuville:",
|
||||
"ApplyTagsHelpTextHowToApplyMovies": "Miten tunnisteita käytetään valituille elokuville:",
|
||||
"Popularity": "Suosio",
|
||||
"IgnoreDownloadsHint": "Estää {appName}ia käsittelemästä näitä latauksia jatkossa.",
|
||||
"NotificationsAppriseSettingsConfigurationKey": "Apprise-määritysavain",
|
||||
@@ -2010,5 +2010,10 @@
|
||||
"ImportListsRadarrSettingsQualityProfilesHelpText": "Lähdeinstanssin laatuprofiilit, joilla tuodaan.",
|
||||
"ImportListsRadarrSettingsRootFoldersHelpText": "Lähdeinstanssin juurikansiot, joista tuodaan.",
|
||||
"ImportListsRadarrSettingsTagsHelpText": "Lähdeinstanssin tunnisteet, joilla tuodaan.",
|
||||
"EditMovieCollectionModalHeader": "Muokataan – {title}"
|
||||
"EditMovieCollectionModalHeader": "Muokataan – {title}",
|
||||
"MovieEditRootFolderHelpText": "Siirtämällä elokuvat niiden nykyiseen juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivittynyttä nimikettä tai nimeämiskaavaa.",
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "Tuotavan {appName}-instanssin URL-osoite portti mukaan lukien (Radarr 3.0 tai uudempi).",
|
||||
"ImportListsRadarrSettingsApiKeyHelpText": "Tuotavan {appName}-instanssin rajapinnan avain (Radarr 3.0 tai uudempi).",
|
||||
"UpdateMoviePath": "Muuta elokuvan sijaintia",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Sisällytä juliste"
|
||||
}
|
||||
|
||||
@@ -907,7 +907,7 @@
|
||||
"NoResultsFound": "Nincs találat",
|
||||
"None": "Egyik sem",
|
||||
"NoMoviesExist": "Nem találhatóak filmek, kezdésnek adjon hozzá egy új filmet vagy importáljon már meglévőket.",
|
||||
"MoveMovieFoldersDontMoveFiles": " Ne, magam mozgatom át a fájlokat",
|
||||
"MoveMovieFoldersDontMoveFiles": "Nem, magam helyezem át a fájlokat",
|
||||
"NoListRecommendations": "Nincs elem a listában és az ajánlások között, kezdésnek adjon hozzá egy filmet, importáljon meglévőket, vagy adjon hozzá egy listát.",
|
||||
"NoLinks": "Nincsenek Linkek",
|
||||
"NoEventsFound": "Nem található események",
|
||||
|
||||
@@ -602,7 +602,7 @@
|
||||
"System": "Sistema",
|
||||
"Sunday": "Domingo",
|
||||
"SuggestTranslationChange": "Sugerir mudança de tradução",
|
||||
"AddNewMovieRootFolderHelpText": "A subpasta \"{0}\" será criada automaticamente",
|
||||
"AddNewMovieRootFolderHelpText": "A subpasta '{folder}' será criada automaticamente",
|
||||
"Style": "Estilo",
|
||||
"Studio": "Studio",
|
||||
"Status": "Status",
|
||||
@@ -717,7 +717,7 @@
|
||||
"ReplaceIllegalCharacters": "Substituir Caracteres Ilegais",
|
||||
"Replace": "Substituir",
|
||||
"Reorder": "Reordenar",
|
||||
"RenameMoviesHelpText": "O {appName} usará o nome de arquivo existente se a renomeação estiver deshabilitada",
|
||||
"RenameMoviesHelpText": "O {appName} usará o nome de arquivo existente se a renomeação estiver desabilitada",
|
||||
"RenameMovies": "Renomear filmes",
|
||||
"RenameFiles": "Renomear Arquivos",
|
||||
"Renamed": "Renomeado",
|
||||
@@ -2014,5 +2014,9 @@
|
||||
"ImportListsRadarrSettingsApiKeyHelpText": "Chave de API da instância {appName} para importar (Radarr 3.0 ou superior)",
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "URL , incluindo porta da instância {appName} para importar (Radarr 3.0 ou superior)",
|
||||
"UpdateMoviePath": "Atualizar Caminho do Filme",
|
||||
"EditMovieCollectionModalHeader": "Editar - {title}"
|
||||
"EditMovieCollectionModalHeader": "Editar - {title}",
|
||||
"NotificationsAppriseSettingsIncludePosterHelpText": "Incluir pôster na mensagem",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Incluir pôster",
|
||||
"MovieFolderFormatHelpTextDeprecatedWarning": "Os tokens associados às propriedades do arquivo de filme foram descontinuados e não terão mais suporte nas principais versões futuras.",
|
||||
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "O formato de pasta do filme não deve conter tokens relacionados a arquivos obsoletos: {tokens}"
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
"InCinemasDate": "Даты в кинотеатрах",
|
||||
"Indexer": "Индексатор",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов",
|
||||
"IndexerLongTermStatusCheckSingleClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {indexerNames}",
|
||||
"IndexerLongTermStatusCheckSingleClientMessage": "Индексаторы недоступны из-за ошибок за последние 6 часов: {indexerNames}",
|
||||
"IndexerPriority": "Приоритет индексатора",
|
||||
"IndexerStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок",
|
||||
"InstallLatest": "Установить последнюю",
|
||||
@@ -833,7 +833,7 @@
|
||||
"TimeFormat": "Формат времени",
|
||||
"Time": "Время",
|
||||
"ThisCannotBeCancelled": "Это действие нельзя отменить после запуска без отключения всех ваших индексаторов.",
|
||||
"TheLogLevelDefault": "Уровень журналирования по умолчанию установлен на 'Информация' и может быть изменён в [Общих настройках](/settings/general)",
|
||||
"TheLogLevelDefault": "Уровень журналирования по умолчанию установлен на 'Отладка' и может быть изменён в [Общих настройках](/settings/general)",
|
||||
"TestAllLists": "Тестировать все листы",
|
||||
"TestAllIndexers": "Тестировать все индексаторы",
|
||||
"TestAllClients": "Тестировать все клиенты",
|
||||
@@ -2014,5 +2014,9 @@
|
||||
"MovieEditRootFolderHelpText": "Перемещение фильмов в ту же корневую директорию может быть полезно для переименования папок для соответствия обновленным названиям или форматам именования",
|
||||
"UpdateMoviePath": "Обновить путь к фильму",
|
||||
"ImportListsRadarrSettingsApiKeyHelpText": "API ключ экземпляра {appName}, откуда импортируются данные (Radarr 3.0 или выше)",
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "URL {appName}, включая порт, откуда импортируются данные (Radarr 3.0 или выше)"
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "URL {appName}, включая порт, откуда импортируются данные (Radarr 3.0 или выше)",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Добавить постер",
|
||||
"MovieFolderFormatHelpTextDeprecatedWarning": "Токены, связанные с параметрами файла фильма, устарели и не будут поддерживаться в грядущих мажорных версиях.",
|
||||
"NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение",
|
||||
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "Формат папки фильма не должен содержать устаревшие токены, связанные с файлом фильма: {tokens}"
|
||||
}
|
||||
|
||||
@@ -2014,5 +2014,6 @@
|
||||
"ImportListsRadarrSettingsFullUrlHelpText": "İçe aktarmak istediğiniz {appName} örneğinin bağlantı noktası da dahil olmak üzere URL'si (Radarr 3.0 veya üzeri)",
|
||||
"MovieEditRootFolderHelpText": "Filmleri aynı kök klasöre taşımak, film klasörlerinin güncellenen başlığa veya adlandırma biçimine uyacak şekilde yeniden adlandırılmasında kullanılabilir",
|
||||
"UpdateMoviePath": "Film Yolunu Güncelle",
|
||||
"EditMovieCollectionModalHeader": "Düzenle - {title}"
|
||||
"EditMovieCollectionModalHeader": "Düzenle - {title}",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Posteri Dahil Et"
|
||||
}
|
||||
|
||||
@@ -2007,5 +2007,6 @@
|
||||
"ImportListsRadarrSettingsRootFoldersHelpText": "Кореневі теки вихідного екземпляра для імпорту",
|
||||
"ImportListsRadarrSettingsTagsHelpText": "Теги з вихідного екземпляра для імпорту",
|
||||
"ImportListsRadarrSettingsFullUrl": "Повний URL-адрес",
|
||||
"EditMovieCollectionModalHeader": "Редагувати - {title}"
|
||||
"EditMovieCollectionModalHeader": "Редагувати - {title}",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Включити постер"
|
||||
}
|
||||
|
||||
@@ -5,5 +5,14 @@
|
||||
"Analytics": "分析",
|
||||
"Username": "用户名",
|
||||
"Activity": "111",
|
||||
"KeyboardShortcutsConfirmModal": "中文"
|
||||
"KeyboardShortcutsConfirmModal": "中文",
|
||||
"Backup": "备份",
|
||||
"Uptime": "运行时间",
|
||||
"YouCanAlsoSearch": "你可以通过TMDB或IMDB的ID进行搜索,如:`tmdb:71663`",
|
||||
"UseProxy": "使用代理",
|
||||
"YesCancel": "确认 ,取消",
|
||||
"Yesterday": "昨天",
|
||||
"Updates": "更新",
|
||||
"Warn": "警告",
|
||||
"BackupNow": "立即备份"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Apprise
|
||||
@@ -20,47 +22,47 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
|
||||
public override void OnGrab(GrabMessage grabMessage)
|
||||
{
|
||||
_proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage.Message, Settings);
|
||||
_proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage.Message, GetPosterUrl(grabMessage.Movie), Settings);
|
||||
}
|
||||
|
||||
public override void OnDownload(DownloadMessage message)
|
||||
{
|
||||
_proxy.SendNotification(MOVIE_DOWNLOADED_TITLE, message.Message, Settings);
|
||||
_proxy.SendNotification(message.OldMovieFiles.Any() ? MOVIE_UPGRADED_TITLE : MOVIE_DOWNLOADED_TITLE, message.Message, GetPosterUrl(message.Movie), Settings);
|
||||
}
|
||||
|
||||
public override void OnMovieAdded(Movie movie)
|
||||
{
|
||||
_proxy.SendNotification(MOVIE_ADDED_TITLE, $"{movie.Title} added to library", Settings);
|
||||
_proxy.SendNotification(MOVIE_ADDED_TITLE, $"{movie.Title} ({movie.Year}) added to library", GetPosterUrl(movie), Settings);
|
||||
}
|
||||
|
||||
public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage)
|
||||
{
|
||||
_proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, Settings);
|
||||
_proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, GetPosterUrl(deleteMessage.Movie), Settings);
|
||||
}
|
||||
|
||||
public override void OnMovieDelete(MovieDeleteMessage deleteMessage)
|
||||
{
|
||||
_proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, Settings);
|
||||
_proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, GetPosterUrl(deleteMessage.Movie), Settings);
|
||||
}
|
||||
|
||||
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
|
||||
{
|
||||
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
|
||||
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, null, Settings);
|
||||
}
|
||||
|
||||
public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
|
||||
{
|
||||
_proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
|
||||
_proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", null, Settings);
|
||||
}
|
||||
|
||||
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
|
||||
{
|
||||
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
|
||||
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, null, Settings);
|
||||
}
|
||||
|
||||
public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message)
|
||||
{
|
||||
_proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, Settings);
|
||||
_proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, GetPosterUrl(message.Movie), Settings);
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -71,5 +73,10 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
private static string GetPosterUrl(Movie movie)
|
||||
{
|
||||
return movie?.MovieMetadata?.Value?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
|
||||
public string Body { get; set; }
|
||||
|
||||
public string Attachment { get; set; }
|
||||
|
||||
public AppriseNotificationType Type { get; set; }
|
||||
|
||||
public string Tag { get; set; }
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
{
|
||||
public interface IAppriseProxy
|
||||
{
|
||||
void SendNotification(string title, string message, AppriseSettings settings);
|
||||
void SendNotification(string title, string message, string posterUrl, AppriseSettings settings);
|
||||
ValidationFailure Test(AppriseSettings settings);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void SendNotification(string title, string message, AppriseSettings settings)
|
||||
public void SendNotification(string title, string message, string posterUrl, AppriseSettings settings)
|
||||
{
|
||||
var payload = new ApprisePayload
|
||||
{
|
||||
@@ -61,6 +61,11 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
payload.Tag = settings.Tags.Join(",");
|
||||
}
|
||||
|
||||
if (settings.IncludePoster && posterUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
payload.Attachment = posterUrl;
|
||||
}
|
||||
|
||||
if (settings.AuthUsername.IsNotNullOrWhiteSpace() || settings.AuthPassword.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.AuthUsername, settings.AuthPassword);
|
||||
@@ -86,10 +91,11 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
{
|
||||
const string title = "Radarr - Test Notification";
|
||||
const string body = "Success! You have properly configured your apprise notification settings.";
|
||||
const string posterUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/128.png";
|
||||
|
||||
try
|
||||
{
|
||||
SendNotification(title, body, settings);
|
||||
SendNotification(title, body, posterUrl, settings);
|
||||
}
|
||||
catch (AppriseException ex) when (ex.InnerException is HttpException httpException)
|
||||
{
|
||||
|
||||
@@ -59,10 +59,13 @@ namespace NzbDrone.Core.Notifications.Apprise
|
||||
[FieldDefinition(5, Label = "NotificationsAppriseSettingsTags", Type = FieldType.Tag, HelpText = "NotificationsAppriseSettingsTagsHelpText")]
|
||||
public IEnumerable<string> Tags { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Username", Type = FieldType.Textbox, HelpText = "NotificationsAppriseSettingsUsernameHelpText", Privacy = PrivacyLevel.UserName)]
|
||||
[FieldDefinition(6, Label = "NotificationsAppriseSettingsIncludePoster", Type = FieldType.Checkbox, HelpText = "NotificationsAppriseSettingsIncludePosterHelpText")]
|
||||
public bool IncludePoster { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Username", Type = FieldType.Textbox, HelpText = "NotificationsAppriseSettingsUsernameHelpText", Privacy = PrivacyLevel.UserName)]
|
||||
public string AuthUsername { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Password", Type = FieldType.Password, HelpText = "NotificationsAppriseSettingsPasswordHelpText", Privacy = PrivacyLevel.Password)]
|
||||
[FieldDefinition(8, Label = "Password", Type = FieldType.Password, HelpText = "NotificationsAppriseSettingsPasswordHelpText", Privacy = PrivacyLevel.Password)]
|
||||
public string AuthPassword { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -70,6 +70,7 @@ namespace NzbDrone.Core.Organizer
|
||||
{ "geo", "kat" },
|
||||
{ "ger", "deu" },
|
||||
{ "gre", "ell" },
|
||||
{ "gsw", "deu" },
|
||||
{ "ice", "isl" },
|
||||
{ "mac", "mkd" },
|
||||
{ "mao", "mri" },
|
||||
@@ -171,16 +172,17 @@ namespace NzbDrone.Core.Organizer
|
||||
namingConfig = _namingConfigService.GetConfig();
|
||||
}
|
||||
|
||||
var movieFile = movie.MovieFile;
|
||||
|
||||
var pattern = namingConfig.MovieFolderFormat;
|
||||
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
|
||||
var multipleTokens = TitleRegex.Matches(pattern).Count > 1;
|
||||
|
||||
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
|
||||
|
||||
AddMovieTokens(tokenHandlers, movie);
|
||||
AddReleaseDateTokens(tokenHandlers, movie.Year);
|
||||
AddIdTokens(tokenHandlers, movie);
|
||||
|
||||
var movieFile = movie.MovieFile;
|
||||
|
||||
if (movie.MovieFile != null)
|
||||
{
|
||||
AddQualityTokens(tokenHandlers, movie, movieFile);
|
||||
|
||||
@@ -19,14 +19,13 @@ namespace NzbDrone.Core.Organizer
|
||||
|
||||
private static MovieFile _movieFile;
|
||||
private static Movie _movie;
|
||||
private static MovieMetadata _movieMetadata;
|
||||
private static List<CustomFormat> _customFormats;
|
||||
|
||||
public FileNameSampleService(IBuildFileNames buildFileNames)
|
||||
{
|
||||
_buildFileNames = buildFileNames;
|
||||
|
||||
var mediaInfo = new MediaInfoModel()
|
||||
var mediaInfo = new MediaInfoModel
|
||||
{
|
||||
VideoFormat = "AVC",
|
||||
VideoBitDepth = 10,
|
||||
@@ -50,23 +49,19 @@ namespace NzbDrone.Core.Organizer
|
||||
Edition = "Ultimate extended edition",
|
||||
};
|
||||
|
||||
_movieMetadata = new MovieMetadata
|
||||
{
|
||||
Title = "The Movie: Title",
|
||||
OriginalTitle = "The Original Movie Title",
|
||||
CollectionTitle = "The Movie Collection",
|
||||
CollectionTmdbId = 123654,
|
||||
Certification = "R",
|
||||
Year = 2010,
|
||||
ImdbId = "tt0066921",
|
||||
TmdbId = 345691
|
||||
};
|
||||
|
||||
_movie = new Movie
|
||||
{
|
||||
MovieFile = _movieFile,
|
||||
MovieFileId = 1,
|
||||
MovieMetadata = _movieMetadata,
|
||||
MovieMetadata = new MovieMetadata
|
||||
{
|
||||
Title = "The Movie: Title",
|
||||
OriginalTitle = "The Original Movie Title",
|
||||
CollectionTitle = "The Movie Collection",
|
||||
CollectionTmdbId = 123654,
|
||||
Certification = "R",
|
||||
Year = 2010,
|
||||
ImdbId = "tt0066921",
|
||||
TmdbId = 345691
|
||||
},
|
||||
MovieMetadataId = 1
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace NzbDrone.Core.Organizer
|
||||
{
|
||||
public static class FileNameValidation
|
||||
{
|
||||
public static readonly Regex DeprecatedMovieFolderTokensRegex = new (@"(\{[- ._\[\(]?(?:Original[- ._](?:Title|Filename)|Release[- ._]Group|Edition[- ._]Tags|Quality[- ._](?:Full|Title|Proper|Real)|MediaInfo[- ._](?:Video|VideoCodec|VideoBitDepth|Audio|AudioCodec|AudioChannels|AudioLanguages|AudioLanguagesAll|SubtitleLanguages|SubtitleLanguagesAll|3D|Simple|Full|VideoDynamicRange|VideoDynamicRangeType))[- ._\]\)]?\})",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static readonly Regex OriginalTokenRegex = new (@"(\{Original[- ._](?:Title|Filename)\})",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace NzbDrone.Core.Organizer
|
||||
|
||||
public class NamingConfigRepository : BasicRepository<NamingConfig>, INamingConfigRepository
|
||||
{
|
||||
protected override bool PublishModelEvents => true;
|
||||
|
||||
public NamingConfigRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
|
||||
@@ -63,7 +63,8 @@ namespace NzbDrone.Core.Parser
|
||||
new IsoLanguage("af", "", "afr", "Afrikaans", Language.Afrikaans),
|
||||
new IsoLanguage("mr", "", "mar", "Marathi", Language.Marathi),
|
||||
new IsoLanguage("tl", "", "tgl", "Tagalog", Language.Tagalog),
|
||||
new IsoLanguage("ur", "", "urd", "Urdu", Language.Urdu)
|
||||
new IsoLanguage("ur", "", "urd", "Urdu", Language.Urdu),
|
||||
new IsoLanguage("rm", "", "roh", "Romansh", Language.Romansh)
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, Language> AlternateIsoCodeMappings = new ()
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Parser
|
||||
|
||||
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_|^)(?<english>\beng\b)|
|
||||
(?<italian>\b(?:ita|italian)\b)|
|
||||
(?<german>german\b|videomann|ger[. ]dub|\bger\b)|
|
||||
(?<german>(?:swiss)?german\b|videomann|ger[. ]dub|\bger\b)|
|
||||
(?<flemish>flemish)|
|
||||
(?<bulgarian>bgaudio)|
|
||||
(?<romanian>rodubbed)|
|
||||
@@ -38,6 +38,7 @@ namespace NzbDrone.Core.Parser
|
||||
(?<japanese>\bJAP\b)|
|
||||
(?<korean>\bKOR\b)|
|
||||
(?<urdu>\burdu\b)|
|
||||
(?<romansh>\b(?:romansh|rumantsch|romansch)\b)|
|
||||
(?<original>\b(?:orig|original)\b)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
@@ -426,6 +427,11 @@ namespace NzbDrone.Core.Parser
|
||||
languages.Add(Language.Urdu);
|
||||
}
|
||||
|
||||
if (match.Groups["romansh"].Success)
|
||||
{
|
||||
languages.Add(Language.Romansh);
|
||||
}
|
||||
|
||||
if (match.Groups["original"].Success)
|
||||
{
|
||||
languages.Add(Language.Original);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.4.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
|
||||
Reference in New Issue
Block a user