diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 6f324ac95..e1566a75c 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -223,7 +223,7 @@ module.exports = (env) => { { loader: 'url-loader', options: { - limit: 10240, + limit: 24096, mimetype: 'application/font-woff', emitFile: false, name: 'Content/Fonts/[name].[ext]' @@ -233,11 +233,12 @@ module.exports = (env) => { }, { - test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, + test: /\.(ttf|eot|eot?#iefix|gif|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: [ { loader: 'file-loader', options: { + limit: 24096, emitFile: false, name: 'Content/Fonts/[name].[ext]' } diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 6ad82854c..881b2599c 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -7,6 +7,7 @@ import QueueConnector from 'Activity/Queue/QueueConnector'; import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector'; import ImportMovies from 'AddMovie/ImportMovie/ImportMovies'; import CalendarPageConnector from 'Calendar/CalendarPageConnector'; +import CollectionConnector from 'Collection/CollectionConnector'; import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector'; @@ -72,6 +73,11 @@ function AppRoutes(props) { component={AddNewMovieConnector} /> + + + + + ); +} + +AddNewCollectionMovieModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddNewCollectionMovieModal; diff --git a/frontend/src/Collection/AddNewCollectionMovieModalContent.css b/frontend/src/Collection/AddNewCollectionMovieModalContent.css new file mode 100644 index 000000000..9c42822be --- /dev/null +++ b/frontend/src/Collection/AddNewCollectionMovieModalContent.css @@ -0,0 +1,68 @@ +.container { + display: flex; +} + +.year { + margin-left: 5px; + color: $disabledColor; +} + +.poster { + flex: 0 0 170px; + margin-right: 20px; + height: 250px; +} + +.info { + flex-grow: 1; +} + +.overview { + margin-bottom: 30px; +} + +.labelIcon { + margin-left: 8px; +} + +.searchForMissingMovieLabelContainer { + display: flex; + margin-top: 2px; +} + +.searchForMissingMovieLabel { + margin-right: 8px; + font-weight: normal; +} + +.searchForMissingMovieContainer { + composes: container from '~Components/Form/CheckInput.css'; + + flex: 0 1 0; +} + +.searchForMissingMovieInput { + composes: input from '~Components/Form/CheckInput.css'; + + margin-top: 0; +} + +.modalFooter { + composes: modalFooter from '~Components/Modal/ModalFooter.css'; +} + +.addButton { + @add-mixin truncate; + composes: button from '~Components/Link/SpinnerButton.css'; +} + +@media only screen and (max-width: $breakpointSmall) { + .modalFooter { + display: block; + text-align: center; + } + + .addButton { + margin-top: 10px; + } +} diff --git a/frontend/src/Collection/AddNewCollectionMovieModalContent.js b/frontend/src/Collection/AddNewCollectionMovieModalContent.js new file mode 100644 index 000000000..545b87675 --- /dev/null +++ b/frontend/src/Collection/AddNewCollectionMovieModalContent.js @@ -0,0 +1,204 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes, kinds } from 'Helpers/Props'; +import MoviePoster from 'Movie/MoviePoster'; +import translate from 'Utilities/String/translate'; +import styles from './AddNewCollectionMovieModalContent.css'; + +class AddNewCollectionMovieModalContent extends Component { + + // + // Listeners + + onQualityProfileIdChange = ({ value }) => { + this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) }); + }; + + onAddMoviePress = () => { + this.props.onAddMoviePress(); + }; + + // + // Render + + render() { + const { + title, + year, + overview, + images, + isAdding, + folder, + tags, + isSmallScreen, + isWindows, + onModalClose, + onInputChange, + rootFolderPath, + monitor, + qualityProfileId, + minimumAvailability, + searchForMovie + } = this.props; + + return ( + + + {title} + + { + !title.contains(year) && !!year && + ({year}) + } + + + +
+ { + !isSmallScreen && +
+ +
+ } + +
+
+ {overview} +
+ +
+ + {translate('RootFolder')} + + + + + + + {translate('Monitor')} + + + + + + + {translate('MinimumAvailability')} + + + + + + {translate('QualityProfile')} + + + + + + {translate('Tags')} + + + +
+
+
+
+ + + + + + {translate('AddMovie')} + + +
+ ); + } +} + +AddNewCollectionMovieModalContent.propTypes = { + title: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + overview: PropTypes.string, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + isAdding: PropTypes.bool.isRequired, + addError: PropTypes.object, + rootFolderPath: PropTypes.object, + monitor: PropTypes.object.isRequired, + qualityProfileId: PropTypes.object, + minimumAvailability: PropTypes.object.isRequired, + searchForMovie: PropTypes.object.isRequired, + folder: PropTypes.string.isRequired, + tags: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isWindows: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired, + onAddMoviePress: PropTypes.func.isRequired +}; + +export default AddNewCollectionMovieModalContent; diff --git a/frontend/src/Collection/AddNewCollectionMovieModalContentConnector.js b/frontend/src/Collection/AddNewCollectionMovieModalContentConnector.js new file mode 100644 index 000000000..740e04322 --- /dev/null +++ b/frontend/src/Collection/AddNewCollectionMovieModalContentConnector.js @@ -0,0 +1,121 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { addMovie, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions'; +import createCollectionSelector from 'Store/Selectors/createCollectionSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import selectSettings from 'Store/Selectors/selectSettings'; +import AddNewMovieModalContent from './AddNewCollectionMovieModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.movieCollections, + createCollectionSelector(), + createDimensionsSelector(), + createSystemStatusSelector(), + (discoverMovieState, collection, dimensions, systemStatus) => { + const { + isAdding, + addError, + pendingChanges + } = discoverMovieState; + + const collectionDefaults = { + rootFolderPath: collection.rootFolderPath, + monitor: 'movieOnly', + qualityProfileId: collection.qualityProfileId, + minimumAvailability: collection.minimumAvailability, + searchForMovie: collection.searchOnAdd, + tags: [] + }; + + const { + settings, + validationErrors, + validationWarnings + } = selectSettings(collectionDefaults, pendingChanges, addError); + + return { + isAdding, + addError, + isSmallScreen: dimensions.isSmallScreen, + validationErrors, + validationWarnings, + isWindows: systemStatus.isWindows, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + addMovie, + setMovieCollectionValue +}; + +class AddNewCollectionMovieModalContentConnector extends Component { + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setMovieCollectionValue({ name, value }); + }; + + onAddMoviePress = () => { + const { + tmdbId, + title, + rootFolderPath, + monitor, + qualityProfileId, + minimumAvailability, + searchForMovie, + tags + } = this.props; + + this.props.addMovie({ + tmdbId, + title, + rootFolderPath: rootFolderPath.value, + monitor: monitor.value, + qualityProfileId: qualityProfileId.value, + minimumAvailability: minimumAvailability.value, + searchForMovie: searchForMovie.value, + tags: tags.value + }); + + this.props.onModalClose(true); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +AddNewCollectionMovieModalContentConnector.propTypes = { + tmdbId: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + rootFolderPath: PropTypes.object, + monitor: PropTypes.object.isRequired, + qualityProfileId: PropTypes.object, + minimumAvailability: PropTypes.object.isRequired, + searchForMovie: PropTypes.object.isRequired, + tags: PropTypes.object.isRequired, + onModalClose: PropTypes.func.isRequired, + addMovie: PropTypes.func.isRequired, + setMovieCollectionValue: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddNewCollectionMovieModalContentConnector); diff --git a/frontend/src/Collection/Collection.js b/frontend/src/Collection/Collection.js new file mode 100644 index 000000000..ebb0c0fab --- /dev/null +++ b/frontend/src/Collection/Collection.js @@ -0,0 +1,403 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageJumpBar from 'Components/Page/PageJumpBar'; +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 { align, icons, sortDirections } from 'Helpers/Props'; +import styles from 'Movie/Index/MovieIndex.css'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; +import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import CollectionFooter from './CollectionFooter'; +import CollectionFilterMenu from './Menus/CollectionFilterMenu'; +import CollectionSortMenu from './Menus/CollectionSortMenu'; +import NoCollection from './NoCollection'; +import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector'; +import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal'; + +function getViewComponent(view) { + return CollectionOverviewsConnector; +} + +class Collection extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + scroller: null, + jumpBarItems: { order: [] }, + jumpToCharacter: null, + isPosterOptionsModalOpen: false, + isOverviewOptionsModalOpen: false, + isConfirmSearchModalOpen: false, + searchType: null, + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + } + + componentDidMount() { + this.setJumpBarItems(); + this.setSelectedState(); + } + + componentDidUpdate(prevProps) { + const { + items, + sortKey, + sortDirection + } = this.props; + + if (sortKey !== prevProps.sortKey || + sortDirection !== prevProps.sortDirection || + hasDifferentItemsOrOrder(prevProps.items, items) + ) { + this.setJumpBarItems(); + this.setSelectedState(); + } + + if (this.state.jumpToCharacter != null) { + this.setState({ jumpToCharacter: null }); + } + } + + // + // Control + + setScrollerRef = (ref) => { + this.setState({ scroller: ref }); + }; + + getSelectedIds = () => { + if (this.state.allUnselected) { + return []; + } + return getSelectedIds(this.state.selectedState); + }; + + setSelectedState() { + const { + items + } = this.props; + + const { + selectedState + } = this.state; + + const newSelectedState = {}; + + items.forEach((collection) => { + const isItemSelected = selectedState[collection.id]; + + if (isItemSelected) { + newSelectedState[collection.id] = isItemSelected; + } else { + newSelectedState[collection.id] = false; + } + }); + + const selectedCount = getSelectedIds(newSelectedState).length; + const newStateCount = Object.keys(newSelectedState).length; + let isAllSelected = false; + let isAllUnselected = false; + + if (selectedCount === 0) { + isAllUnselected = true; + } else if (selectedCount === newStateCount) { + isAllSelected = true; + } + + this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected }); + } + + setJumpBarItems() { + const { + items, + sortKey, + sortDirection + } = this.props; + + // Reset if not sorting by sortTitle + if (sortKey !== 'sortTitle') { + this.setState({ jumpBarItems: { order: [] } }); + return; + } + + const characters = _.reduce(items, (acc, item) => { + let char = item.sortTitle.charAt(0); + + if (!isNaN(char)) { + char = '#'; + } + + if (char in acc) { + acc[char] = acc[char] + 1; + } else { + acc[char] = 1; + } + + return acc; + }, {}); + + const order = Object.keys(characters).sort(); + + // Reverse if sorting descending + if (sortDirection === sortDirections.DESCENDING) { + order.reverse(); + } + + const jumpBarItems = { + characters, + order + }; + + this.setState({ jumpBarItems }); + } + + // + // Listeners + + onOverviewOptionsPress = () => { + this.setState({ isOverviewOptionsModalOpen: true }); + }; + + onOverviewOptionsModalClose = () => { + this.setState({ isOverviewOptionsModalOpen: false }); + }; + + onJumpBarItemPress = (jumpToCharacter) => { + this.setState({ jumpToCharacter }); + }; + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + }; + + onSelectAllPress = () => { + this.onSelectAllChange({ value: !this.state.allSelected }); + }; + + onRefreshMovieCollectionsPress = () => { + this.props.onRefreshMovieCollectionsPress(); + }; + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey, 'id'); + }); + }; + + onUpdateSelectedPress = (changes) => { + this.props.onUpdateSelectedPress({ + collectionIds: this.getSelectedIds(), + ...changes + }); + }; + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + totalItems, + items, + selectedFilterKey, + filters, + customFilters, + sortKey, + sortDirection, + view, + onSortSelect, + onFilterSelect, + onScroll, + isRefreshingCollections, + isSaving, + isAdding, + ...otherProps + } = this.props; + + const { + scroller, + jumpBarItems, + jumpToCharacter, + isOverviewOptionsModalOpen, + selectedState, + allSelected, + allUnselected + } = this.state; + + const selectedMovieIds = this.getSelectedIds(); + + const ViewComponent = getViewComponent(view); + const isLoaded = !!(!error && isPopulated && items.length && scroller); + const hasNoCollection = !totalItems; + + return ( + + + + + + + + + { + view === 'overview' ? + : + null + } + + { + (view === 'posters' || view === 'overview') && + + } + + + + + + + +
+ + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
+ {translate('UnableToLoadCollections')} +
+ } + + { + isLoaded && +
+ +
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + { + isLoaded && !!jumpBarItems.order.length && + + } +
+ + { + isLoaded && + + } + + +
+ ); + } +} + +Collection.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + isAdding: PropTypes.bool.isRequired, + error: PropTypes.object, + totalItems: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + view: PropTypes.string.isRequired, + isRefreshingCollections: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired, + onUpdateSelectedPress: PropTypes.func.isRequired, + onRefreshMovieCollectionsPress: PropTypes.func.isRequired +}; + +export default Collection; diff --git a/frontend/src/Collection/CollectionConnector.js b/frontend/src/Collection/CollectionConnector.js new file mode 100644 index 000000000..2deb3fb8e --- /dev/null +++ b/frontend/src/Collection/CollectionConnector.js @@ -0,0 +1,108 @@ +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 withScrollPosition from 'Components/withScrollPosition'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import scrollPositions from 'Store/scrollPositions'; +import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import Collection from './Collection'; + +function createMapStateToProps() { + return createSelector( + createCollectionClientSideCollectionItemsSelector('movieCollections'), + createCommandExecutingSelector(commandNames.REFRESH_COLLECTIONS), + createDimensionsSelector(), + ( + collections, + isRefreshingCollections, + dimensionsState + ) => { + return { + ...collections, + isRefreshingCollections, + isSmallScreen: dimensionsState.isSmallScreen + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchRootFolders() { + dispatch(fetchRootFolders()); + }, + onUpdateSelectedPress(payload) { + dispatch(saveMovieCollections(payload)); + }, + onSortSelect(sortKey) { + dispatch(setMovieCollectionsSort({ sortKey })); + }, + onFilterSelect(selectedFilterKey) { + dispatch(setMovieCollectionsFilter({ selectedFilterKey })); + }, + onRefreshMovieCollectionsPress() { + dispatch(executeCommand({ + name: commandNames.REFRESH_COLLECTIONS + })); + } + }; +} + +class CollectionConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.dispatchFetchRootFolders(); + } + + componentWillUnmount() { + unregisterPagePopulator(this.repopulate); + } + + // + // Listeners + + onScroll = ({ scrollTop }) => { + scrollPositions.movieCollections = scrollTop; + }; + + onUpdateSelectedPress = (payload) => { + this.props.onUpdateSelectedPress(payload); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +CollectionConnector.propTypes = { + isSmallScreen: PropTypes.bool.isRequired, + view: PropTypes.string.isRequired, + onUpdateSelectedPress: PropTypes.func.isRequired, + dispatchFetchRootFolders: PropTypes.func.isRequired +}; + +export default withScrollPosition( + connect(createMapStateToProps, createMapDispatchToProps)(CollectionConnector), + 'movieCollections' +); diff --git a/frontend/src/Collection/CollectionFilterModalConnector.js b/frontend/src/Collection/CollectionFilterModalConnector.js new file mode 100644 index 000000000..0fa897451 --- /dev/null +++ b/frontend/src/Collection/CollectionFilterModalConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.movieCollections.items, + (state) => state.movieCollections.filterBuilderProps, + (sectionItems, filterBuilderProps) => { + return { + sectionItems, + filterBuilderProps, + customFilterType: 'movieCollections' + }; + } + ); +} + +const mapDispatchToProps = { + dispatchSetFilter: setMovieCollectionsFilter +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal); diff --git a/frontend/src/Collection/CollectionFooter.css b/frontend/src/Collection/CollectionFooter.css new file mode 100644 index 000000000..c9cf4ce4c --- /dev/null +++ b/frontend/src/Collection/CollectionFooter.css @@ -0,0 +1,56 @@ +.inputContainer { + margin-right: 20px; + min-width: 150px; +} + +.buttonContainer { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.buttonContainerContent { + flex-grow: 0; +} + +.buttons { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.addSelectedButton { + composes: button from '~Components/Link/SpinnerButton.css'; + + margin-right: 10px; + height: 35px; +} + +.excludeSelectedButton { + composes: button from '~Components/Link/SpinnerButton.css'; + + margin-left: 25px; + height: 35px; +} + +@media only screen and (max-width: $breakpointSmall) { + .inputContainer { + margin-right: 0; + } + + .buttonContainer { + justify-content: flex-start; + } + + .buttonContainerContent { + flex-grow: 1; + } + + .buttons { + justify-content: space-between; + } + + .selectedMovieLabel { + text-align: left; + } +} diff --git a/frontend/src/Collection/CollectionFooter.js b/frontend/src/Collection/CollectionFooter.js new file mode 100644 index 000000000..3f5016e5f --- /dev/null +++ b/frontend/src/Collection/CollectionFooter.js @@ -0,0 +1,163 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import SelectInput from 'Components/Form/SelectInput'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import CollectionFooterLabel from './CollectionFooterLabel'; +import styles from './CollectionFooter.css'; + +const NO_CHANGE = 'noChange'; + +class CollectionFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + monitor: NO_CHANGE, + monitored: NO_CHANGE, + destinationRootFolder: null + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = this.props; + + const newState = {}; + if (prevProps.isSaving && !isSaving && !saveError) { + this.setState({ + monitored: NO_CHANGE, + monitor: NO_CHANGE + }); + } + + if (!_.isEmpty(newState)) { + this.setState(newState); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + }; + + onUpdateSelectedPress = () => { + const { + monitor, + monitored + } = this.state; + + const changes = {}; + + if (monitored !== NO_CHANGE) { + changes.monitored = monitored === 'monitored'; + } + + if (monitor !== NO_CHANGE) { + changes.monitor = monitor; + } + + this.props.onUpdateSelectedPress(changes); + }; + + // + // Render + + render() { + const { + selectedIds, + isSaving + } = this.props; + + const { + monitored, + monitor + } = this.state; + + const monitoredOptions = [ + { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, + { key: 'monitored', value: translate('Monitored') }, + { key: 'unmonitored', value: translate('Unmonitored') } + ]; + + const selectedCount = selectedIds.length; + + return ( + +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ + {translate('UpdateSelected')} + +
+
+
+
+
+ ); + } +} + +CollectionFooter.propTypes = { + selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired, + isAdding: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + onUpdateSelectedPress: PropTypes.func.isRequired +}; + +export default CollectionFooter; diff --git a/frontend/src/Collection/CollectionFooterLabel.css b/frontend/src/Collection/CollectionFooterLabel.css new file mode 100644 index 000000000..9b4b40be6 --- /dev/null +++ b/frontend/src/Collection/CollectionFooterLabel.css @@ -0,0 +1,8 @@ +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.savingIcon { + margin-left: 8px; +} diff --git a/frontend/src/Collection/CollectionFooterLabel.js b/frontend/src/Collection/CollectionFooterLabel.js new file mode 100644 index 000000000..6f8b578df --- /dev/null +++ b/frontend/src/Collection/CollectionFooterLabel.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import { icons } from 'Helpers/Props'; +import styles from './CollectionFooterLabel.css'; + +function CollectionFooterLabel(props) { + const { + className, + label, + isSaving + } = props; + + return ( +
+ {label} + + { + isSaving && + + } +
+ ); +} + +CollectionFooterLabel.propTypes = { + className: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + isSaving: PropTypes.bool.isRequired +}; + +CollectionFooterLabel.defaultProps = { + className: styles.label +}; + +export default CollectionFooterLabel; diff --git a/frontend/src/Collection/CollectionItemConnector.js b/frontend/src/Collection/CollectionItemConnector.js new file mode 100644 index 000000000..d94fa6345 --- /dev/null +++ b/frontend/src/Collection/CollectionItemConnector.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; +import createCollectionSelector from 'Store/Selectors/createCollectionSelector'; + +function createMapStateToProps() { + return createSelector( + createCollectionSelector(), + createAllMoviesSelector(), + ( + collection, + allMovies + ) => { + // If a movie is deleted this selector may fire before the parent + // selecors, which will result in an undefined movie, if that happens + // we want to return early here and again in the render function to avoid + // trying to show a movie that has no information available. + + if (!collection) { + return {}; + } + + let allGenres = []; + let libraryMovies = 0; + + collection.movies.forEach((movie) => { + allGenres = allGenres.concat(movie.genres); + + if (allMovies.find((libraryMovie) => libraryMovie.tmdbId === movie.tmdbId)) { + libraryMovies++; + } + }); + + return { + ...collection, + genres: Array.from(new Set(allGenres)).slice(0, 3), + missingMovies: collection.movies.length - libraryMovies + }; + } + ); +} + +class CollectionItemConnector extends Component { + + // + // Render + + render() { + const { + id, + component: ItemComponent, + ...otherProps + } = this.props; + + if (!id) { + return null; + } + + return ( + + ); + } +} + +CollectionItemConnector.propTypes = { + id: PropTypes.number, + component: PropTypes.elementType.isRequired +}; + +export default connect(createMapStateToProps)(CollectionItemConnector); diff --git a/frontend/src/Collection/Edit/EditCollectionModal.js b/frontend/src/Collection/Edit/EditCollectionModal.js new file mode 100644 index 000000000..1017aad0e --- /dev/null +++ b/frontend/src/Collection/Edit/EditCollectionModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditCollectionModalContentConnector from './EditCollectionModalContentConnector'; + +function EditCollectionModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditCollectionModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditCollectionModal; diff --git a/frontend/src/Collection/Edit/EditCollectionModalConnector.js b/frontend/src/Collection/Edit/EditCollectionModalConnector.js new file mode 100644 index 000000000..c73e7f186 --- /dev/null +++ b/frontend/src/Collection/Edit/EditCollectionModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditCollectionModal from './EditCollectionModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditCollectionModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'movieCollections' }); + this.props.onModalClose(); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +EditCollectionModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector); diff --git a/frontend/src/Collection/Edit/EditCollectionModalContent.css b/frontend/src/Collection/Edit/EditCollectionModalContent.css new file mode 100644 index 000000000..9d99248e8 --- /dev/null +++ b/frontend/src/Collection/Edit/EditCollectionModalContent.css @@ -0,0 +1,17 @@ +.container { + display: flex; +} + +.poster { + flex: 0 0 170px; + margin-right: 20px; + height: 250px; +} + +.info { + flex-grow: 1; +} + +.overview { + margin-bottom: 30px; +} diff --git a/frontend/src/Collection/Edit/EditCollectionModalContent.js b/frontend/src/Collection/Edit/EditCollectionModalContent.js new file mode 100644 index 000000000..093c30d5d --- /dev/null +++ b/frontend/src/Collection/Edit/EditCollectionModalContent.js @@ -0,0 +1,178 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes } from 'Helpers/Props'; +import MoviePoster from 'Movie/MoviePoster'; +import translate from 'Utilities/String/translate'; +import styles from './EditCollectionModalContent.css'; + +class EditCollectionModalContent extends Component { + + // + // Listeners + + onSavePress = () => { + const { + onSavePress + } = this.props; + + onSavePress(false); + }; + + // + // Render + + render() { + const { + title, + images, + overview, + item, + isSaving, + onInputChange, + onModalClose, + isSmallScreen, + ...otherProps + } = this.props; + + const { + monitored, + qualityProfileId, + minimumAvailability, + // Id, + rootFolderPath, + searchOnAdd + } = item; + + return ( + + + {translate('Edit')} - {title} + + + +
+ { + !isSmallScreen && +
+ +
+ } + +
+
+ {overview} +
+ +
+ + {translate('Monitored')} + + + + + + {translate('MinimumAvailability')} + + + + + + {translate('QualityProfile')} + + + + + + {translate('Folder')} + + + + + + {translate('SearchOnAdd')} + + + +
+
+
+
+ + + + + + {translate('Save')} + + +
+ ); + } +} + +EditCollectionModalContent.propTypes = { + collectionId: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + overview: PropTypes.string.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + item: PropTypes.object.isRequired, + isSaving: PropTypes.bool.isRequired, + isPathChanging: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditCollectionModalContent; diff --git a/frontend/src/Collection/Edit/EditCollectionModalContentConnector.js b/frontend/src/Collection/Edit/EditCollectionModalContentConnector.js new file mode 100644 index 000000000..9600647b2 --- /dev/null +++ b/frontend/src/Collection/Edit/EditCollectionModalContentConnector.js @@ -0,0 +1,119 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { saveMovieCollection, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions'; +import createCollectionSelector from 'Store/Selectors/createCollectionSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import selectSettings from 'Store/Selectors/selectSettings'; +import EditCollectionModalContent from './EditCollectionModalContent'; + +function createIsPathChangingSelector() { + return createSelector( + (state) => state.movieCollections.pendingChanges, + createCollectionSelector(), + (pendingChanges, collection) => { + const rootFolderPath = pendingChanges.rootFolderPath; + + if (rootFolderPath == null) { + return false; + } + + return collection.rootFolderPath !== rootFolderPath; + } + ); +} + +function createMapStateToProps() { + return createSelector( + (state) => state.movieCollections, + createCollectionSelector(), + createIsPathChangingSelector(), + createDimensionsSelector(), + (moviesState, collection, isPathChanging, dimensions) => { + const { + isSaving, + saveError, + pendingChanges + } = moviesState; + + const movieSettings = { + monitored: collection.monitored, + qualityProfileId: collection.qualityProfileId, + minimumAvailability: collection.minimumAvailability, + rootFolderPath: collection.rootFolderPath, + searchOnAdd: collection.searchOnAdd + }; + + const settings = selectSettings(movieSettings, pendingChanges, saveError); + + return { + title: collection.title, + images: collection.images, + overview: collection.overview, + isSaving, + saveError, + isPathChanging, + originalPath: collection.path, + item: settings.settings, + isSmallScreen: dimensions.isSmallScreen, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + dispatchSetMovieCollectionValue: setMovieCollectionValue, + dispatchSaveMovieCollection: saveMovieCollection +}; + +class EditCollectionModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.dispatchSetMovieCollectionValue({ name, value }); + }; + + onSavePress = () => { + this.props.dispatchSaveMovieCollection({ + id: this.props.collectionId + }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +EditCollectionModalContentConnector.propTypes = { + collectionId: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + dispatchSetMovieCollectionValue: PropTypes.func.isRequired, + dispatchSaveMovieCollection: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditCollectionModalContentConnector); diff --git a/frontend/src/Collection/Menus/CollectionFilterMenu.js b/frontend/src/Collection/Menus/CollectionFilterMenu.js new file mode 100644 index 000000000..652f2accf --- /dev/null +++ b/frontend/src/Collection/Menus/CollectionFilterMenu.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import CollectionFilterModalConnector from 'Collection/CollectionFilterModalConnector'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import { align } from 'Helpers/Props'; + +function CollectionFilterMenu(props) { + const { + selectedFilterKey, + filters, + customFilters, + isDisabled, + onFilterSelect + } = props; + + return ( + + ); +} + +CollectionFilterMenu.propTypes = { + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + isDisabled: PropTypes.bool.isRequired, + onFilterSelect: PropTypes.func.isRequired +}; + +CollectionFilterMenu.defaultProps = { + showCustomFilters: false +}; + +export default CollectionFilterMenu; diff --git a/frontend/src/Collection/Menus/CollectionSortMenu.js b/frontend/src/Collection/Menus/CollectionSortMenu.js new file mode 100644 index 000000000..9738fa69f --- /dev/null +++ b/frontend/src/Collection/Menus/CollectionSortMenu.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import MenuContent from 'Components/Menu/MenuContent'; +import SortMenu from 'Components/Menu/SortMenu'; +import SortMenuItem from 'Components/Menu/SortMenuItem'; +import { align, sortDirections } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +function CollectionSortMenu(props) { + const { + sortKey, + sortDirection, + isDisabled, + onSortSelect + } = props; + + return ( + + + + {translate('Title')} + + + + ); +} + +CollectionSortMenu.propTypes = { + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + isDisabled: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired +}; + +export default CollectionSortMenu; diff --git a/frontend/src/Collection/NoCollection.css b/frontend/src/Collection/NoCollection.css new file mode 100644 index 000000000..38a01f391 --- /dev/null +++ b/frontend/src/Collection/NoCollection.css @@ -0,0 +1,11 @@ +.message { + margin-top: 10px; + margin-bottom: 30px; + text-align: center; + font-size: 20px; +} + +.buttonContainer { + margin-top: 20px; + text-align: center; +} diff --git a/frontend/src/Collection/NoCollection.js b/frontend/src/Collection/NoCollection.js new file mode 100644 index 000000000..1e76fd014 --- /dev/null +++ b/frontend/src/Collection/NoCollection.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from 'Components/Link/Button'; +import { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './NoCollection.css'; + +function NoCollection(props) { + const { totalItems } = props; + + if (totalItems > 0) { + return ( +
+
+ {translate('AllCollectionsHiddenDueToFilter')} +
+
+ ); + } + + return ( +
+
+ {translate('NoCollections')} +
+ +
+ +
+ +
+ +
+
+ ); +} + +NoCollection.propTypes = { + totalItems: PropTypes.number.isRequired +}; + +export default NoCollection; diff --git a/frontend/src/Collection/Overview/CollectionMovie.css b/frontend/src/Collection/Overview/CollectionMovie.css new file mode 100644 index 000000000..1d9ee93fe --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionMovie.css @@ -0,0 +1,117 @@ +$hoverScale: 1.05; + +.content { + border-radius: 5px; + transition: all 200ms ease-in; + + &:hover { + z-index: 2; + box-shadow: 0 0 10px $black; + transition: all 200ms ease-in; + + .poster { + opacity: 0.5; + transition: opacity 100ms linear 100ms; + } + + .overlayTitle { + opacity: 1; + transition: opacity 100ms linear 100ms; + } + } +} + +.posterContainer { + position: relative; +} + +.poster { + position: relative; + display: block; + background-color: $defaultColor; +} + +.overlay { + position: absolute; + top: 0; + left: 0; + display: flex; + justify-content: flex-end; + flex-direction: column; + width: 100%; + height: 100%; +} + +.overlayTitle { + padding: 5px; + color: $offWhite; + text-align: left; + font-weight: bold; + font-size: 15px; + opacity: 0; + transition: opacity 0; +} + +.title { + @add-mixin truncate; + + background-color: #fafbfc; + text-align: center; + font-size: $smallFontSize; +} + +.controls { + position: absolute; + bottom: 10px; + left: 10px; + z-index: 3; + border-radius: 4px; + background-color: #707070; + color: $white; + font-size: $smallFontSize; + opacity: 0; + transition: opacity 0; +} + +.action { + composes: button from '~Components/Link/IconButton.css'; + + &:hover { + color: $radarrYellow; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .container { + padding: 5px; + } +} + +.editorSelect { + position: absolute; + top: 10px; + z-index: 3; +} + +.externalLinks { + margin-left: 0.5em; +} + +.link { + composes: link from '~Components/Link/Link.css'; + + position: relative; + display: block; + background-color: $defaultColor; +} + +.monitorToggleButton { + composes: toggleButton from '~Components/MonitorToggleButton.css'; + + width: 25px; + color: $white; + + &:hover { + color: $iconButtonHoverLightColor; + } +} diff --git a/frontend/src/Collection/Overview/CollectionMovie.js b/frontend/src/Collection/Overview/CollectionMovie.js new file mode 100644 index 000000000..50b1b5c4a --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionMovie.js @@ -0,0 +1,191 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; +import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; +import MoviePoster from 'Movie/MoviePoster'; +import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal'; +import styles from './CollectionMovie.css'; + +class CollectionMovie extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPosterError: false, + isEditMovieModalOpen: false, + isNewAddMovieModalOpen: false + }; + } + + // + // Listeners + + onEditMoviePress = () => { + this.setState({ isEditMovieModalOpen: true }); + }; + + onEditMovieModalClose = () => { + this.setState({ isEditMovieModalOpen: false }); + }; + + onAddMoviePress = () => { + this.setState({ isNewAddMovieModalOpen: true }); + }; + + onAddMovieModalClose = () => { + this.setState({ isNewAddMovieModalOpen: false }); + }; + + onPosterLoad = () => { + if (this.state.hasPosterError) { + this.setState({ hasPosterError: false }); + } + }; + + onPosterLoadError = () => { + if (!this.state.hasPosterError) { + this.setState({ hasPosterError: true }); + } + }; + + // + // Render + + render() { + const { + id, + title, + overview, + year, + tmdbId, + images, + monitored, + hasFile, + folder, + isAvailable, + isExistingMovie, + posterWidth, + posterHeight, + detailedProgressBar, + onMonitorTogglePress, + collectionId + } = this.props; + + const { + isEditMovieModalOpen, + isNewAddMovieModalOpen + } = this.state; + + const linkProps = id ? { to: `/movie/${tmdbId}` } : { onPress: this.onAddMoviePress }; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px`, + borderRadius: '5px' + }; + + return ( +
+
+ { + isExistingMovie && +
+ +
+ } + + + + +
+
+ {title} +
+ + { + id && +
+ +
+ } +
+ +
+ + + + +
+ ); + } +} + +CollectionMovie.propTypes = { + id: PropTypes.number, + title: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + overview: PropTypes.string.isRequired, + monitored: PropTypes.bool, + collectionId: PropTypes.number.isRequired, + hasFile: PropTypes.bool, + folder: PropTypes.string, + isAvailable: PropTypes.bool, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + isExistingMovie: PropTypes.bool, + tmdbId: PropTypes.number.isRequired, + imdbId: PropTypes.string, + youTubeTrailerId: PropTypes.string, + onMonitorTogglePress: PropTypes.func.isRequired +}; + +export default CollectionMovie; diff --git a/frontend/src/Collection/Overview/CollectionMovieConnector.js b/frontend/src/Collection/Overview/CollectionMovieConnector.js new file mode 100644 index 000000000..0a2800b4e --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionMovieConnector.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { toggleMovieMonitored } from 'Store/Actions/movieActions'; +import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import CollectionMovie from './CollectionMovie'; + +function createMapStateToProps() { + return createSelector( + createDimensionsSelector(), + createCollectionExistingMovieSelector(), + (dimensions, existingMovie) => { + return { + isSmallScreen: dimensions.isSmallScreen, + isExistingMovie: !!existingMovie, + ...existingMovie + }; + } + ); +} + +const mapDispatchToProps = { + toggleMovieMonitored +}; + +class CollectionMovieConnector extends Component { + + // + // Listeners + + onMonitorTogglePress = (monitored) => { + this.props.toggleMovieMonitored({ + movieId: this.props.id, + monitored + }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +CollectionMovieConnector.propTypes = { + id: PropTypes.number, + monitored: PropTypes.bool, + toggleMovieMonitored: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieConnector); diff --git a/frontend/src/Collection/Overview/CollectionOverview.css b/frontend/src/Collection/Overview/CollectionOverview.css new file mode 100644 index 000000000..fc7eef288 --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionOverview.css @@ -0,0 +1,132 @@ +$hoverScale: 1.05; + +.content { + display: flex; + flex-grow: 1; +} + +.editorSelect { + position: relative; + top: 0; + left: 5px; +} + +.titleRow { + position: relative; + display: flex; + justify-content: space-between; + flex: 0 0 auto; +} + +.toggleMonitoredContainer { + align-self: center; + margin-right: 10px; +} + +.titleContainer { + display: flex; + margin-bottom: 5px; +} + +.sliderContainer { + display: block; +} + +.moviesContainer { + margin-bottom: 5px; +} + +.movie { + padding: 7px; +} + +.info { + display: flex; + flex: 1 0 1px; + flex-direction: column; + overflow: hidden; + padding-left: 10px; +} + +.title { + @add-mixin truncate; + + font-weight: 300; + font-size: 30px; + line-height: 40px; +} + +.actions { + white-space: nowrap; +} + +.details { + display: flex; + justify-content: space-between; + flex: 1 0 auto; + margin-bottom: 5px; +} + +.defaults { + margin-bottom: 5px; + font-weight: 300; + font-size: 20px; +} + +.detailsLabel { + composes: label from '~Components/Label.css'; + + margin: 5px 10px 5px 0; +} + +.path, +.status, +.genres, +.qualityProfileName { + margin-left: 8px; + font-weight: 300; + font-size: 15px; +} + +.overview { + overflow: hidden; + min-height: 0; +} + +.monitorToggleButton { + composes: toggleButton from '~Components/MonitorToggleButton.css'; + + width: 25px; + + &:hover { + color: $iconButtonHoverLightColor; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .navigationButtons { + display: none; + } + + .title { + @add-mixin truncate; + + font-weight: 300; + font-size: 22px; + } + + .toggleMonitoredContainer { + align-self: center; + margin-right: 5px; + } + + .monitorToggleButton { + composes: toggleButton from '~Components/MonitorToggleButton.css'; + + width: 20px; + + &:hover { + color: $iconButtonHoverLightColor; + } + } +} diff --git a/frontend/src/Collection/Overview/CollectionOverview.js b/frontend/src/Collection/Overview/CollectionOverview.js new file mode 100644 index 000000000..1931140ef --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionOverview.js @@ -0,0 +1,321 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Slider from 'react-slick'; +import TextTruncate from 'react-text-truncate'; +import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector'; +import CheckInput from 'Components/Form/CheckInput'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import { icons, sizes } from 'Helpers/Props'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import dimensions from 'Styles/Variables/dimensions'; +import fonts from 'Styles/Variables/fonts'; +import translate from 'Utilities/String/translate'; +import CollectionMovieConnector from './CollectionMovieConnector'; +import styles from './CollectionOverview.css'; + +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; + +const columnPadding = parseInt(dimensions.movieIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); +const defaultFontSize = parseInt(fonts.defaultFontSize); +const lineHeight = parseFloat(fonts.lineHeight); + +// Hardcoded height beased on line-height of 32 + bottom margin of 10. 19 + 5 for List Row +// Less side-effecty than using react-measure. +const titleRowHeight = 100; + +function getContentHeight(rowHeight, isSmallScreen) { + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + + return rowHeight - padding; +} + +class CollectionOverview extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditCollectionModalOpen: false, + isNewAddMovieModalOpen: false + }; + } + + // + // Control + + setSliderRef = (ref) => { + this.setState({ slider: ref }); + }; + + // + // Listeners + + onPress = () => { + this.setState({ isNewAddMovieModalOpen: true }); + }; + + onEditCollectionPress = () => { + this.setState({ isEditCollectionModalOpen: true }); + }; + + onEditCollectionModalClose = () => { + this.setState({ isEditCollectionModalOpen: false }); + }; + + onAddMovieModalClose = () => { + this.setState({ isNewAddMovieModalOpen: false }); + }; + + onChange = ({ value, shiftKey }) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + }; + + // + // Render + + render() { + const { + monitored, + qualityProfileId, + rootFolderPath, + genres, + id, + title, + movies, + overview, + missingMovies, + posterHeight, + posterWidth, + rowHeight, + isSmallScreen, + isSelected, + onMonitorTogglePress + } = this.props; + + const { + showDetails, + showOverview, + detailedProgressBar + } = this.props.overviewOptions; + + const { + isEditCollectionModalOpen + } = this.state; + + const contentHeight = getContentHeight(rowHeight, isSmallScreen); + const overviewHeight = contentHeight - titleRowHeight - posterHeight; + + const sliderSettings = { + arrows: false, + dots: false, + infinite: false, + slidesToShow: 1, + slidesToScroll: 1, + variableWidth: true + }; + + return ( +
+
+
+ +
+
+ +
+
+
+ +
+
+ {title} +
+ + +
+ +
+ + + +
+
+ + { + showDetails && +
+ + + { + !isSmallScreen && + + } + + { + !isSmallScreen && + + } + + { + !isSmallScreen && + + } + +
+ } + + { + showOverview && +
+
+ +
+
+ } + +
+ + {movies.map((movie) => ( +
+ +
+ ))} +
+
+
+
+ + +
+ ); + } +} + +CollectionOverview.propTypes = { + id: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + qualityProfileId: PropTypes.number.isRequired, + minimumAvailability: PropTypes.string.isRequired, + searchOnAdd: PropTypes.bool.isRequired, + rootFolderPath: PropTypes.string.isRequired, + tmdbId: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + overview: PropTypes.string.isRequired, + movies: PropTypes.arrayOf(PropTypes.object).isRequired, + genres: PropTypes.arrayOf(PropTypes.string).isRequired, + missingMovies: PropTypes.number.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + rowHeight: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + posterWidth: PropTypes.number.isRequired, + overviewOptions: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + onMonitorTogglePress: PropTypes.func.isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +export default CollectionOverview; diff --git a/frontend/src/Collection/Overview/CollectionOverviewConnector.js b/frontend/src/Collection/Overview/CollectionOverviewConnector.js new file mode 100644 index 000000000..3b004f401 --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionOverviewConnector.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import CollectionOverview from './CollectionOverview'; + +function createMapStateToProps() { + return createSelector( + createDimensionsSelector(), + (dimensions) => { + return { + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +const mapDispatchToProps = { + toggleCollectionMonitored +}; + +class CollectionOverviewConnector extends Component { + + // + // Listeners + + onMonitorTogglePress = (monitored) => { + this.props.toggleCollectionMonitored({ + collectionId: this.props.collectionId, + monitored + }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +CollectionOverviewConnector.propTypes = { + collectionId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + toggleCollectionMonitored: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CollectionOverviewConnector); diff --git a/frontend/src/Collection/Overview/CollectionOverviews.css b/frontend/src/Collection/Overview/CollectionOverviews.css new file mode 100644 index 000000000..a0cee09ff --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionOverviews.css @@ -0,0 +1,15 @@ +.grid { + flex: 1 0 auto; +} + +.container { + &:hover { + .content { + background-color: $tableRowHoverBackgroundColor; + } + } +} + +.externalLinks { + margin-right: 0.5em; +} diff --git a/frontend/src/Collection/Overview/CollectionOverviews.js b/frontend/src/Collection/Overview/CollectionOverviews.js new file mode 100644 index 000000000..01c5d9c64 --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionOverviews.js @@ -0,0 +1,272 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Grid, WindowScroller } from 'react-virtualized'; +import CollectionItemConnector from 'Collection/CollectionItemConnector'; +import Measure from 'Components/Measure'; +import dimensions from 'Styles/Variables/dimensions'; +import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; +import CollectionOverviewConnector from './CollectionOverviewConnector'; +import styles from './CollectionOverviews.css'; + +// Poster container dimensions +const columnPadding = parseInt(dimensions.movieIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); + +function calculatePosterWidth(posterSize, isSmallScreen) { + const maxiumPosterWidth = isSmallScreen ? 152 : 162; + + if (posterSize === 'large') { + return maxiumPosterWidth; + } + + if (posterSize === 'medium') { + return Math.floor(maxiumPosterWidth * 0.75); + } + + return Math.floor(maxiumPosterWidth * 0.5); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) { + + const heights = [ + posterHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + return heights.reduce((acc, height) => acc + height + 80, 0); +} + +function calculatePosterHeight(posterWidth) { + return Math.ceil((250 / 170) * posterWidth); +} + +class CollectionOverviews extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnCount: 1, + posterWidth: 162, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}) + }; + + this._grid = null; + } + + componentDidUpdate(prevProps, prevState) { + const { + items, + sortKey, + overviewOptions, + jumpToCharacter, + scrollTop, + isSmallScreen + } = this.props; + + const { + width, + rowHeight, + scrollRestored + } = this.state; + + if (prevProps.sortKey !== sortKey || + prevProps.overviewOptions !== overviewOptions) { + this.calculateGrid(this.state.width, isSmallScreen); + } + + if ( + this._grid && + (prevState.width !== width || + prevState.rowHeight !== rowHeight || + hasDifferentItemsOrOrder(prevProps.items, items) || + prevProps.overviewOptions !== overviewOptions)) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells + this._grid.recomputeGridSize(); + } + + if (this._grid && scrollTop !== 0 && !scrollRestored) { + this.setState({ scrollRestored: true }); + this._grid.scrollToPosition({ scrollTop }); + } + + if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { + const index = getIndexOfFirstCharacter(items, jumpToCharacter); + + if (this._grid && index != null) { + + this._grid.scrollToCell({ + rowIndex: index, + columnIndex: 0 + }); + } + } + } + + // + // Control + + setGridRef = (ref) => { + this._grid = ref; + }; + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + overviewOptions + } = this.props; + + const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen); + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions); + + this.setState({ + width, + posterWidth, + posterHeight, + rowHeight + }); + }; + + cellRenderer = ({ key, rowIndex, style }) => { + const { + items, + sortKey, + overviewOptions, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + isSmallScreen, + selectedState, + onSelectedChange + } = this.props; + + const { + posterWidth, + posterHeight, + rowHeight + } = this.state; + + const collection = items[rowIndex]; + + if (!collection) { + return null; + } + + return ( +
+ +
+ ); + }; + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + }; + + // + // Render + + render() { + const { + isSmallScreen, + scroller, + items, + selectedState + } = this.props; + + const { + width, + rowHeight + } = this.state; + + return ( + + + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return
; + } + + return ( +
+ +
+ ); + } + } + + + ); + } +} + +CollectionOverviews.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + overviewOptions: PropTypes.object.isRequired, + jumpToCharacter: PropTypes.string, + scrollTop: PropTypes.number.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + selectedState: PropTypes.object.isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +export default CollectionOverviews; diff --git a/frontend/src/Collection/Overview/CollectionOverviewsConnector.js b/frontend/src/Collection/Overview/CollectionOverviewsConnector.js new file mode 100644 index 000000000..05385eac1 --- /dev/null +++ b/frontend/src/Collection/Overview/CollectionOverviewsConnector.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import CollectionOverviews from './CollectionOverviews'; + +function createMapStateToProps() { + return createSelector( + (state) => state.movieCollections.overviewOptions, + createUISettingsSelector(), + createDimensionsSelector(), + (overviewOptions, uiSettings, dimensions) => { + return { + overviewOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(CollectionOverviews); diff --git a/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModal.js b/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModal.js new file mode 100644 index 000000000..b7773fbac --- /dev/null +++ b/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import CollectionOverviewOptionsModalContentConnector from './CollectionOverviewOptionsModalContentConnector'; + +function CollectionOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +CollectionOverviewOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default CollectionOverviewOptionsModal; diff --git a/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModalContent.js b/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModalContent.js new file mode 100644 index 000000000..881826e1a --- /dev/null +++ b/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModalContent.js @@ -0,0 +1,183 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +const posterSizeOptions = [ + { key: 'small', value: translate('Small') }, + { key: 'medium', value: translate('Medium') }, + { key: 'large', value: translate('Large') } +]; + +class CollectionOverviewOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showDetails: props.showDetails, + showOverview: props.showOverview + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showDetails, + showOverview + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showDetails !== prevProps.showDetails) { + state.showDetails = showDetails; + } + + if (showOverview !== prevProps.showOverview) { + state.showOverview = showOverview; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangeOverviewOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangeOverviewOption({ [name]: value }); + }); + }; + + onChangeOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangeOption({ + [name]: value + }); + }); + }; + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + size, + detailedProgressBar, + showDetails, + showOverview + } = this.state; + + return ( + + + Overview Options + + + +
+ + {translate('PosterSize')} + + + + + + {translate('DetailedProgressBar')} + + + + + + {translate('ShowCollectionDetails')} + + + + + + {translate('ShowOverview')} + + + +
+
+ + + + +
+ ); + } +} + +CollectionOverviewOptionsModalContent.propTypes = { + detailedProgressBar: PropTypes.bool.isRequired, + size: PropTypes.string.isRequired, + showDetails: PropTypes.bool.isRequired, + showOverview: PropTypes.bool.isRequired, + onChangeOverviewOption: PropTypes.func.isRequired, + onChangeOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default CollectionOverviewOptionsModalContent; diff --git a/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModalContentConnector.js b/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModalContentConnector.js new file mode 100644 index 000000000..aaa4ecb0b --- /dev/null +++ b/frontend/src/Collection/Overview/Options/CollectionOverviewOptionsModalContentConnector.js @@ -0,0 +1,29 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setMovieCollectionsOption, setMovieCollectionsOverviewOption } from 'Store/Actions/movieCollectionActions'; +import CollectionOverviewOptionsModalContent from './CollectionOverviewOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.movieCollections, + (movieCollections) => { + return { + ...movieCollections.options, + ...movieCollections.overviewOptions + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangeOverviewOption(payload) { + dispatch(setMovieCollectionsOverviewOption(payload)); + }, + onChangeOption(payload) { + dispatch(setMovieCollectionsOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(CollectionOverviewOptionsModalContent); diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js index b91bf665c..bb48d3393 100644 --- a/frontend/src/Commands/commandNames.js +++ b/frontend/src/Commands/commandNames.js @@ -10,6 +10,7 @@ export const DOWNLOADED_MOVIES_SCAN = 'DownloadedMoviesScan'; export const INTERACTIVE_IMPORT = 'ManualImport'; export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch'; export const MOVE_MOVIE = 'MoveMovie'; +export const REFRESH_COLLECTIONS = 'RefreshCollections'; export const REFRESH_MOVIE = 'RefreshMovie'; export const RENAME_FILES = 'RenameFiles'; export const RENAME_MOVIE = 'RenameMovie'; diff --git a/frontend/src/Components/Form/MovieMonitoredSelectInput.js b/frontend/src/Components/Form/MovieMonitoredSelectInput.js index fcb2ff9ff..dcf1de0c9 100644 --- a/frontend/src/Components/Form/MovieMonitoredSelectInput.js +++ b/frontend/src/Components/Form/MovieMonitoredSelectInput.js @@ -1,15 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import translate from 'Utilities/String/translate'; +import monitorOptions from 'Utilities/Movie/monitorOptions'; import SelectInput from './SelectInput'; -const monitorTypesOptions = [ - { key: 'true', value: translate('Yes') }, - { key: 'false', value: translate('No') } -]; - function MovieMonitoredSelectInput(props) { - const values = [...monitorTypesOptions]; + const values = [...monitorOptions]; const { includeNoChange, diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index 4706c9dd2..4c25771d3 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -6,6 +6,7 @@ import { createSelector } from 'reselect'; import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchMovies } from 'Store/Actions/movieActions'; +import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions'; import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchTags } from 'Store/Actions/tagActions'; @@ -51,6 +52,7 @@ const selectIsPopulated = createSelector( (state) => state.settings.indexerFlags.isPopulated, (state) => state.settings.importLists.isPopulated, (state) => state.system.status.isPopulated, + (state) => state.movieCollections.isPopulated, ( customFiltersIsPopulated, tagsIsPopulated, @@ -59,7 +61,8 @@ const selectIsPopulated = createSelector( languagesIsPopulated, indexerFlagsIsPopulated, importListsIsPopulated, - systemStatusIsPopulated + systemStatusIsPopulated, + movieCollectionsIsPopulated ) => { return ( customFiltersIsPopulated && @@ -69,7 +72,8 @@ const selectIsPopulated = createSelector( languagesIsPopulated && indexerFlagsIsPopulated && importListsIsPopulated && - systemStatusIsPopulated + systemStatusIsPopulated && + movieCollectionsIsPopulated ); } ); @@ -83,6 +87,7 @@ const selectErrors = createSelector( (state) => state.settings.indexerFlags.error, (state) => state.settings.importLists.error, (state) => state.system.status.error, + (state) => state.movieCollections.error, ( customFiltersError, tagsError, @@ -91,7 +96,8 @@ const selectErrors = createSelector( languagesError, indexerFlagsError, importListsError, - systemStatusError + systemStatusError, + movieCollectionsError ) => { const hasError = !!( customFiltersError || @@ -101,7 +107,8 @@ const selectErrors = createSelector( languagesError || indexerFlagsError || importListsError || - systemStatusError + systemStatusError || + movieCollectionsError ); return { @@ -113,7 +120,8 @@ const selectErrors = createSelector( languagesError, indexerFlagsError, importListsError, - systemStatusError + systemStatusError, + movieCollectionsError }; } ); @@ -148,6 +156,9 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchMovies() { dispatch(fetchMovies()); }, + dispatchFetchMovieCollections() { + dispatch(fetchMovieCollections()); + }, dispatchFetchCustomFilters() { dispatch(fetchCustomFilters()); }, @@ -197,6 +208,7 @@ class PageConnector extends Component { componentDidMount() { if (!this.props.isPopulated) { this.props.dispatchFetchMovies(); + this.props.dispatchFetchMovieCollections(); this.props.dispatchFetchCustomFilters(); this.props.dispatchFetchTags(); this.props.dispatchFetchQualityProfiles(); @@ -223,6 +235,7 @@ class PageConnector extends Component { isPopulated, hasError, dispatchFetchMovies, + dispatchFetchMovieCollections, dispatchFetchTags, dispatchFetchQualityProfiles, dispatchFetchLanguages, @@ -262,6 +275,7 @@ PageConnector.propTypes = { hasError: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired, dispatchFetchMovies: PropTypes.func.isRequired, + dispatchFetchMovieCollections: PropTypes.func.isRequired, dispatchFetchCustomFilters: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired, diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 53c6ab327..257509ce5 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -33,6 +33,10 @@ const links = [ title: translate('ImportLibrary'), to: '/add/import' }, + { + title: translate('Collections'), + to: '/collections' + }, { title: translate('Discover'), to: '/add/discover' diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index 53a8daccd..de78045d2 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -203,6 +203,19 @@ class SignalRConnector extends Component { } }; + handleCollection = (body) => { + const action = body.action; + const section = 'movieCollections'; + + console.log(body); + + if (action === 'updated') { + this.props.dispatchUpdateItem({ section, ...body.resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: body.resource.id }); + } + }; + handleQueue = () => { if (this.props.isQueuePopulated) { this.props.dispatchFetchQueue(); diff --git a/frontend/src/DiscoverMovie/DiscoverMovieFooter.js b/frontend/src/DiscoverMovie/DiscoverMovieFooter.js index de4dd4a9c..a3961f518 100644 --- a/frontend/src/DiscoverMovie/DiscoverMovieFooter.js +++ b/frontend/src/DiscoverMovie/DiscoverMovieFooter.js @@ -9,6 +9,7 @@ import SelectInput from 'Components/Form/SelectInput'; import SpinnerButton from 'Components/Link/SpinnerButton'; import PageContentFooter from 'Components/Page/PageContentFooter'; import { kinds } from 'Helpers/Props'; +import monitorOptions from 'Utilities/Movie/monitorOptions'; import translate from 'Utilities/String/translate'; import DiscoverMovieFooterLabel from './DiscoverMovieFooterLabel'; import ExcludeMovieModal from './Exclusion/ExcludeMovieModal'; @@ -137,11 +138,6 @@ class DiscoverMovieFooter extends Component { isExcludeMovieModalOpen } = this.state; - const monitoredOptions = [ - { key: true, value: translate('Monitored') }, - { key: false, value: translate('Unmonitored') } - ]; - return (
@@ -153,7 +149,7 @@ class DiscoverMovieFooter extends Component { diff --git a/frontend/src/DiscoverMovie/Posters/DiscoverMoviePosterInfo.js b/frontend/src/DiscoverMovie/Posters/DiscoverMoviePosterInfo.js index a1b2b2b87..4e8d97ce2 100644 --- a/frontend/src/DiscoverMovie/Posters/DiscoverMoviePosterInfo.js +++ b/frontend/src/DiscoverMovie/Posters/DiscoverMoviePosterInfo.js @@ -129,7 +129,7 @@ DiscoverMoviePosterInfo.propTypes = { digitalRelease: PropTypes.string, physicalRelease: PropTypes.string, runtime: PropTypes.number, - ratings: PropTypes.arrayOf(PropTypes.object).isRequired, + ratings: PropTypes.object.isRequired, sortKey: PropTypes.string.isRequired, showRelativeDates: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, diff --git a/frontend/src/DiscoverMovie/Table/DiscoverMovieRow.js b/frontend/src/DiscoverMovie/Table/DiscoverMovieRow.js index d4d30b6ba..9d6e12e4b 100644 --- a/frontend/src/DiscoverMovie/Table/DiscoverMovieRow.js +++ b/frontend/src/DiscoverMovie/Table/DiscoverMovieRow.js @@ -164,7 +164,7 @@ class DiscoverMovieRow extends Component { key={name} className={styles[name]} > - {collection ? collection.name : null } + {collection ? collection.title : null } ); } @@ -373,7 +373,7 @@ DiscoverMovieRow.propTypes = { digitalRelease: PropTypes.string, runtime: PropTypes.number, genres: PropTypes.arrayOf(PropTypes.string).isRequired, - ratings: PropTypes.arrayOf(PropTypes.object).isRequired, + ratings: PropTypes.object.isRequired, certification: PropTypes.string, collection: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index 59b9e8f94..bc9a3577f 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -39,7 +39,7 @@ import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; -import MovieCollectionConnector from './../MovieCollectionConnector'; +import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector'; import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector'; import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector'; import MovieDetailsLinks from './MovieDetailsLinks'; @@ -583,10 +583,8 @@ class MovieDetails extends Component { size={sizes.LARGE} >
-
diff --git a/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.css b/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.css index b98bb33d5..a2680291a 100644 --- a/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.css +++ b/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.css @@ -7,6 +7,15 @@ transition: width 200ms ease; } +.progressRadius { + composes: container from '~Components/ProgressBar.css'; + + border-radius: 0 0 5px 5px; + background-color: #5b5b5b; + color: $white; + transition: width 200ms ease; +} + .progressBar { composes: progressBar from '~Components/ProgressBar.css'; diff --git a/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.js b/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.js index 314fb618b..888b0c05d 100644 --- a/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.js +++ b/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.js @@ -15,6 +15,7 @@ function MovieIndexProgressBar(props) { isAvailable, posterWidth, detailedProgressBar, + bottomRadius, queueStatus, queueState } = props; @@ -40,7 +41,7 @@ function MovieIndexProgressBar(props) { return ( - {collection ? collection.name : null } + {collection ? collection.title : null } ); } diff --git a/frontend/src/Movie/Index/Table/MovieIndexTable.js b/frontend/src/Movie/Index/Table/MovieIndexTable.js index 147ba9e0b..77e77ee8b 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexTable.js +++ b/frontend/src/Movie/Index/Table/MovieIndexTable.js @@ -65,6 +65,7 @@ class MovieIndexTable extends Component { component={MovieIndexRow} columns={columns} movieId={movie.id} + collectionId={movie.collectionId} qualityProfileId={movie.qualityProfileId} isSelected={selectedState[movie.id]} onSelectedChange={onSelectedChange} diff --git a/frontend/src/Movie/MovieCollection.js b/frontend/src/Movie/MovieCollection.js deleted file mode 100644 index 86e560dfb..000000000 --- a/frontend/src/Movie/MovieCollection.js +++ /dev/null @@ -1,73 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import MonitorToggleButton from 'Components/MonitorToggleButton'; -import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector'; -import styles from './MovieCollection.css'; - -class MovieCollection extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - hasPosterError: false, - isEditImportListModalOpen: false - }; - } - - onAddImportListPress = (monitored) => { - if (this.props.collectionList) { - this.props.onMonitorTogglePress(monitored); - } else { - this.props.onMonitorTogglePress(monitored); - this.setState({ isEditImportListModalOpen: true }); - } - }; - - onEditImportListModalClose = () => { - this.setState({ isEditImportListModalOpen: false }); - }; - - render() { - const { - name, - collectionList, - isSaving - } = this.props; - - const monitored = collectionList !== undefined && collectionList.enabled && collectionList.enableAuto; - const importListId = collectionList ? collectionList.id : 0; - - return ( -
- - {name} - -
- ); - } -} - -MovieCollection.propTypes = { - tmdbId: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - collectionList: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - onMonitorTogglePress: PropTypes.func.isRequired -}; - -export default MovieCollection; diff --git a/frontend/src/Movie/MovieCollectionConnector.js b/frontend/src/Movie/MovieCollectionConnector.js deleted file mode 100644 index e62a380de..000000000 --- a/frontend/src/Movie/MovieCollectionConnector.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveImportList, selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions'; -import createMovieCollectionListSelector from 'Store/Selectors/createMovieCollectionListSelector'; -import createMovieSelector from 'Store/Selectors/createMovieSelector'; -import MovieCollection from './MovieCollection'; - -function createMapStateToProps() { - return createSelector( - createMovieSelector(), - createMovieCollectionListSelector(), - (state) => state.settings.importLists, - (movie, collectionList, importLists) => { - const { - monitored, - qualityProfileId, - minimumAvailability - } = movie; - - return { - collectionList, - monitored, - qualityProfileId, - minimumAvailability, - isSaving: importLists.isSaving - }; - } - ); -} - -const mapDispatchToProps = { - selectImportListSchema, - setImportListFieldValue, - setImportListValue, - saveImportList -}; - -class MovieCollectionConnector extends Component { - - // - // Listeners - - onMonitorTogglePress = (monitored) => { - if (this.props.collectionList) { - this.props.setImportListValue({ name: 'enabled', value: monitored }); - this.props.setImportListValue({ name: 'enableAuto', value: monitored }); - this.props.saveImportList({ id: this.props.collectionList.id }); - } else { - this.props.selectImportListSchema({ implementation: 'TMDbCollectionImport', presetName: undefined }); - this.props.setImportListFieldValue({ name: 'collectionId', value: this.props.tmdbId.toString() }); - this.props.setImportListValue({ name: 'enabled', value: true }); - this.props.setImportListValue({ name: 'enableAuto', value: true }); - this.props.setImportListValue({ name: 'name', value: `${this.props.name} - ${this.props.tmdbId}` }); - this.props.setImportListValue({ name: 'qualityProfileId', value: this.props.qualityProfileId }); - this.props.setImportListValue({ name: 'monitored', value: this.props.monitored }); - this.props.setImportListValue({ name: 'minimumAvailability', value: this.props.minimumAvailability }); - } - }; - - // - // Render - - render() { - return ( - - ); - } -} - -MovieCollectionConnector.propTypes = { - tmdbId: PropTypes.number.isRequired, - movieId: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - collectionList: PropTypes.object, - monitored: PropTypes.bool.isRequired, - qualityProfileId: PropTypes.number.isRequired, - minimumAvailability: PropTypes.string.isRequired, - isSaving: PropTypes.bool.isRequired, - selectImportListSchema: PropTypes.func.isRequired, - setImportListFieldValue: PropTypes.func.isRequired, - setImportListValue: PropTypes.func.isRequired, - saveImportList: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionConnector); diff --git a/frontend/src/Movie/MovieCollection.css b/frontend/src/Movie/MovieCollectionLabel.css similarity index 100% rename from frontend/src/Movie/MovieCollection.css rename to frontend/src/Movie/MovieCollectionLabel.css diff --git a/frontend/src/Movie/MovieCollectionLabel.js b/frontend/src/Movie/MovieCollectionLabel.js new file mode 100644 index 000000000..fb071f91c --- /dev/null +++ b/frontend/src/Movie/MovieCollectionLabel.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import styles from './MovieCollectionLabel.css'; + +class MovieCollectionLabel extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPosterError: false + }; + } + + render() { + const { + title, + monitored, + onMonitorTogglePress + } = this.props; + + return ( +
+ + {title} +
+ ); + } +} + +MovieCollectionLabel.propTypes = { + title: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + onMonitorTogglePress: PropTypes.func.isRequired +}; + +export default MovieCollectionLabel; diff --git a/frontend/src/Movie/MovieCollectionLabelConnector.js b/frontend/src/Movie/MovieCollectionLabelConnector.js new file mode 100644 index 000000000..3d41e51e5 --- /dev/null +++ b/frontend/src/Movie/MovieCollectionLabelConnector.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions'; +import MovieCollectionLabel from './MovieCollectionLabel'; + +function createMapStateToProps() { + return createSelector( + (state, { tmdbId }) => tmdbId, + (state) => state.movieCollections.items, + (tmdbId, collections) => { + const collection = collections.find((movie) => movie.tmdbId === tmdbId); + return { + ...collection + }; + } + ); +} + +const mapDispatchToProps = { + toggleCollectionMonitored +}; + +class MovieCollectionLabelConnector extends Component { + + // + // Listeners + + onMonitorTogglePress = (monitored) => { + this.props.toggleCollectionMonitored({ + collectionId: this.props.id, + monitored + }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +MovieCollectionLabelConnector.propTypes = { + tmdbId: PropTypes.number.isRequired, + id: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + toggleCollectionMonitored: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionLabelConnector); diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index c3636aebb..2cc8eb7e5 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -42,7 +42,7 @@ function EditImportListModalContent(props) { name, enabled, enableAuto, - shouldMonitor, + monitor, minimumAvailability, qualityProfileId, rootFolderPath, @@ -121,31 +121,28 @@ function EditImportListModalContent(props) { - {translate('AddMoviesMonitored')} + {translate('Monitor')} - { - shouldMonitor && - - {translate('SearchOnAdd')} + + {translate('SearchOnAdd')} - - - } + + {translate('MinimumAvailability')} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js index 581882ffd..354b73e70 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions'; import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import sortByName from 'Utilities/Array/sortByName'; @@ -17,7 +18,8 @@ function createMapStateToProps() { const mapDispatchToProps = { dispatchFetchQualityProfiles: fetchQualityProfiles, dispatchDeleteQualityProfile: deleteQualityProfile, - dispatchCloneQualityProfile: cloneQualityProfile + dispatchCloneQualityProfile: cloneQualityProfile, + dispatchFetchMovieCollections: fetchMovieCollections }; class QualityProfilesConnector extends Component { @@ -27,6 +29,7 @@ class QualityProfilesConnector extends Component { componentDidMount() { this.props.dispatchFetchQualityProfiles(); + this.props.dispatchFetchMovieCollections(); } // @@ -57,7 +60,8 @@ class QualityProfilesConnector extends Component { QualityProfilesConnector.propTypes = { dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchDeleteQualityProfile: PropTypes.func.isRequired, - dispatchCloneQualityProfile: PropTypes.func.isRequired + dispatchCloneQualityProfile: PropTypes.func.isRequired, + dispatchFetchMovieCollections: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector); diff --git a/frontend/src/Store/Actions/addMovieActions.js b/frontend/src/Store/Actions/addMovieActions.js index a9d1cc893..c65698e99 100644 --- a/frontend/src/Store/Actions/addMovieActions.js +++ b/frontend/src/Store/Actions/addMovieActions.js @@ -30,7 +30,7 @@ export const defaultState = { defaults: { rootFolderPath: '', - monitor: 'true', + monitor: 'movieOnly', qualityProfileId: 0, minimumAvailability: 'announced', searchForMovie: true, diff --git a/frontend/src/Store/Actions/discoverMovieActions.js b/frontend/src/Store/Actions/discoverMovieActions.js index 3b2f85b9e..ab621653b 100644 --- a/frontend/src/Store/Actions/discoverMovieActions.js +++ b/frontend/src/Store/Actions/discoverMovieActions.js @@ -46,7 +46,7 @@ export const defaultState = { defaults: { rootFolderPath: '', - monitor: 'true', + monitor: 'movieOnly', qualityProfileId: 0, minimumAvailability: 'announced', searchForMovie: true, @@ -188,7 +188,7 @@ export const defaultState = { collection: function(item) { const { collection ={} } = item; - return collection.name; + return collection.title; }, studio: function(item) { diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index a9c3c5e35..09c373494 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -12,6 +12,7 @@ import * as importMovie from './importMovieActions'; import * as interactiveImportActions from './interactiveImportActions'; import * as movies from './movieActions'; import * as movieBlocklist from './movieBlocklistActions'; +import * as movieCollections from './movieCollectionActions'; import * as movieCredits from './movieCreditsActions'; import * as movieFiles from './movieFileActions'; import * as movieHistory from './movieHistoryActions'; @@ -50,6 +51,7 @@ export default [ rootFolders, movies, movieBlocklist, + movieCollections, movieHistory, movieIndex, movieCredits, diff --git a/frontend/src/Store/Actions/movieActions.js b/frontend/src/Store/Actions/movieActions.js index 2e9183f89..d44c7e3b4 100644 --- a/frontend/src/Store/Actions/movieActions.js +++ b/frontend/src/Store/Actions/movieActions.js @@ -157,8 +157,6 @@ export const filterPredicates = { imdbRating: function(item, filterValue, type) { const predicate = filterTypePredicates[type]; - console.log(item.ratings); - const rating = item.ratings.imdb ? item.ratings.imdb.value : 0; return predicate(rating, filterValue); diff --git a/frontend/src/Store/Actions/movieCollectionActions.js b/frontend/src/Store/Actions/movieCollectionActions.js new file mode 100644 index 000000000..d9c836acb --- /dev/null +++ b/frontend/src/Store/Actions/movieCollectionActions.js @@ -0,0 +1,347 @@ +import _ from 'lodash'; +import { createAction } from 'redux-actions'; +import { batchActions } from 'redux-batched-actions'; +import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import getNewMovie from 'Utilities/Movie/getNewMovie'; +import { set, update, updateItem } from './baseActions'; +import createHandleActions from './Creators/createHandleActions'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer'; + +// +// Variables + +export const section = 'movieCollections'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + isAdding: false, + addError: null, + sortKey: 'sortTitle', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortTitle', + secondarySortDirection: sortDirections.ASCENDING, + view: 'overview', + pendingChanges: {}, + + overviewOptions: { + detailedProgressBar: false, + size: 'medium', + showDetails: true, + showOverview: true + }, + + defaults: { + rootFolderPath: '', + monitor: 'movieOnly', + qualityProfileId: 0, + minimumAvailability: 'announced', + searchForMovie: true, + tags: [] + }, + + selectedFilterKey: 'all', + + filters: [ + { + key: 'all', + label: 'All', + filters: [] + } + ], + + filterPredicates: {}, + + filterBuilderProps: [ + { + name: 'title', + label: 'Title', + type: filterBuilderTypes.STRING + }, + { + name: 'monitored', + label: 'Monitored', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL + } + ] +}; + +export const persistState = [ + 'movieCollections.defaults', + 'movieCollections.sortKey', + 'movieCollections.sortDirection', + 'movieCollections.selectedFilterKey', + 'movieCollections.customFilters', + 'movieCollections.options', + 'movieCollections.overviewOptions' +]; + +// +// Actions Types + +export const FETCH_MOVIE_COLLECTIONS = 'movieCollections/fetchMovieCollections'; +export const CLEAR_MOVIE_COLLECTIONS = 'movieCollections/clearMovieCollections'; +export const SAVE_MOVIE_COLLECTION = 'movieCollections/saveMovieCollection'; +export const SAVE_MOVIE_COLLECTIONS = 'movieCollections/saveMovieCollections'; +export const SET_MOVIE_COLLECTION_VALUE = 'movieCollections/setMovieCollectionValue'; + +export const ADD_MOVIE = 'movieCollections/addMovie'; + +export const TOGGLE_COLLECTION_MONITORED = 'movieCollections/toggleCollectionMonitored'; + +export const SET_MOVIE_COLLECTIONS_SORT = 'movieCollections/setMovieCollectionsSort'; +export const SET_MOVIE_COLLECTIONS_FILTER = 'movieCollections/setMovieCollectionsFilter'; +export const SET_MOVIE_COLLECTIONS_OPTION = 'movieCollections/setMovieCollectionsOption'; +export const SET_MOVIE_COLLECTIONS_OVERVIEW_OPTION = 'movieCollections/setMovieCollectionsOverviewOption'; + +// +// Action Creators + +export const fetchMovieCollections = createThunk(FETCH_MOVIE_COLLECTIONS); +export const clearMovieCollections = createAction(CLEAR_MOVIE_COLLECTIONS); +export const saveMovieCollection = createThunk(SAVE_MOVIE_COLLECTION); +export const saveMovieCollections = createThunk(SAVE_MOVIE_COLLECTIONS); + +export const addMovie = createThunk(ADD_MOVIE); + +export const toggleCollectionMonitored = createThunk(TOGGLE_COLLECTION_MONITORED); + +export const setMovieCollectionsSort = createAction(SET_MOVIE_COLLECTIONS_SORT); +export const setMovieCollectionsFilter = createAction(SET_MOVIE_COLLECTIONS_FILTER); +export const setMovieCollectionsOption = createAction(SET_MOVIE_COLLECTIONS_OPTION); +export const setMovieCollectionsOverviewOption = createAction(SET_MOVIE_COLLECTIONS_OVERVIEW_OPTION); + +export const setMovieCollectionValue = createAction(SET_MOVIE_COLLECTION_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [SAVE_MOVIE_COLLECTION]: createSaveProviderHandler(section, '/collection'), + [FETCH_MOVIE_COLLECTIONS]: function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); + + const promise = createAjaxRequest({ + url: '/collection', + data: payload + }).request; + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }, + + [ADD_MOVIE]: function(getState, payload, dispatch) { + dispatch(set({ section, isAdding: true })); + + const tmdbId = payload.tmdbId; + const title = payload.title; + + const newMovie = getNewMovie({ tmdbId, title }, payload); + newMovie.id = 0; + + const promise = createAjaxRequest({ + url: '/movie', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(newMovie) + }).request; + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ section: 'movies', ...data }), + + set({ + section, + isAdding: false, + isAdded: true, + addError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isAdding: false, + isAdded: false, + addError: xhr + })); + }); + }, + + [TOGGLE_COLLECTION_MONITORED]: (getState, payload, dispatch) => { + const { + collectionId: id, + monitored + } = payload; + + const collection = _.find(getState().movieCollections.items, { id }); + + dispatch(updateItem({ + id, + section, + isSaving: true + })); + + const promise = createAjaxRequest({ + url: `/collection/${id}`, + method: 'PUT', + data: JSON.stringify({ + ...collection, + monitored + }), + dataType: 'json' + }).request; + + promise.done((data) => { + dispatch(updateItem({ + id, + section, + isSaving: false, + monitored + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section, + isSaving: false + })); + }); + }, + + [SAVE_MOVIE_COLLECTIONS]: function(getState, payload, dispatch) { + const { + collectionIds, + monitored, + monitor + } = payload; + + const response = {}; + const collections = []; + + collectionIds.forEach((id) => { + const collectionToUpdate = { id }; + + if (payload.hasOwnProperty('monitored')) { + collectionToUpdate.monitored = monitored; + } + + collections.push(collectionToUpdate); + }); + + if (payload.hasOwnProperty('monitor')) { + response.monitorMovies = monitor === 'monitored'; + } + + response.collections = collections; + + dispatch(set({ + section, + isSaving: true + })); + + const promise = createAjaxRequest({ + url: '/collection', + method: 'PUT', + data: JSON.stringify(response), + dataType: 'json' + }).request; + + promise.done((data) => { + dispatch(fetchMovieCollections()); + + dispatch(set({ + section, + isSaving: false, + saveError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_MOVIE_COLLECTIONS_SORT]: createSetClientSideCollectionSortReducer(section), + [SET_MOVIE_COLLECTIONS_FILTER]: createSetClientSideCollectionFilterReducer(section), + [SET_MOVIE_COLLECTION_VALUE]: createSetSettingValueReducer(section), + + [SET_MOVIE_COLLECTIONS_OPTION]: function(state, { payload }) { + const movieCollectionsOptions = state.options; + + return { + ...state, + options: { + ...movieCollectionsOptions, + ...payload + } + }; + }, + + [SET_MOVIE_COLLECTIONS_OVERVIEW_OPTION]: function(state, { payload }) { + const overviewOptions = state.overviewOptions; + + return { + ...state, + overviewOptions: { + ...overviewOptions, + ...payload + } + }; + }, + + [CLEAR_MOVIE_COLLECTIONS]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Migrators/migrateMonitorToEnum.js b/frontend/src/Store/Migrators/migrateMonitorToEnum.js new file mode 100644 index 000000000..2953b9afc --- /dev/null +++ b/frontend/src/Store/Migrators/migrateMonitorToEnum.js @@ -0,0 +1,26 @@ +import get from 'lodash'; + +export default function migrateMonitorToEnum(persistedState) { + const addMovie = get(persistedState, 'addMovie.defaults.monitor'); + const discoverMovie = get(persistedState, 'discoverMovie.defaults.monitor'); + + if (!addMovie && !discoverMovie) { + return; + } + + if (addMovie === true) { + persistedState.addMovie.defaults.monitor = 'movieOnly'; + } + + if (discoverMovie === true) { + persistedState.discoverMovie.defaults.monitor = 'movieOnly'; + } + + if (addMovie === false) { + persistedState.addMovie.defaults.monitor = 'none'; + } + + if (discoverMovie === false) { + persistedState.discoverMovie.defaults.monitor = 'none'; + } +} diff --git a/frontend/src/Store/Selectors/createCollectionClientSideCollectionItemsSelector.js b/frontend/src/Store/Selectors/createCollectionClientSideCollectionItemsSelector.js new file mode 100644 index 000000000..fd925a81c --- /dev/null +++ b/frontend/src/Store/Selectors/createCollectionClientSideCollectionItemsSelector.js @@ -0,0 +1,45 @@ +import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; +import createClientSideCollectionSelector from './createClientSideCollectionSelector'; + +function createUnoptimizedSelector(uiSection) { + return createSelector( + createClientSideCollectionSelector('movieCollections', uiSection), + (movies) => { + const items = movies.items.map((s) => { + const { + id, + sortTitle + } = s; + + return { + id, + sortTitle + }; + }); + + return { + ...movies, + items + }; + } + ); +} + +function movieListEqual(a, b) { + return hasDifferentItemsOrOrder(a, b); +} + +const createMovieEqualSelector = createSelectorCreator( + defaultMemoize, + movieListEqual +); + +function createCollectionClientSideCollectionItemsSelector(uiSection) { + return createMovieEqualSelector( + createUnoptimizedSelector(uiSection), + (movies) => movies + ); +} + +export default createCollectionClientSideCollectionItemsSelector; diff --git a/frontend/src/Store/Selectors/createCollectionExistingMovieSelector.js b/frontend/src/Store/Selectors/createCollectionExistingMovieSelector.js new file mode 100644 index 000000000..024d4e939 --- /dev/null +++ b/frontend/src/Store/Selectors/createCollectionExistingMovieSelector.js @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import createAllMoviesSelector from './createAllMoviesSelector'; + +function createCollectionExistingMovieSelector() { + return createSelector( + (state, { tmdbId }) => tmdbId, + createAllMoviesSelector(), + (tmdbId, allMovies) => { + return allMovies.find((movie) => movie.tmdbId === tmdbId); + } + ); +} + +export default createCollectionExistingMovieSelector; diff --git a/frontend/src/Store/Selectors/createCollectionSelector.js b/frontend/src/Store/Selectors/createCollectionSelector.js new file mode 100644 index 000000000..0b8717f96 --- /dev/null +++ b/frontend/src/Store/Selectors/createCollectionSelector.js @@ -0,0 +1,17 @@ +import { createSelector } from 'reselect'; + +function createCollectionSelector() { + return createSelector( + (state, { collectionId }) => collectionId, + (state) => state.movieCollections.itemMap, + (state) => state.movieCollections.items, + (collectionId, itemMap, allCollections) => { + if (allCollections && itemMap && collectionId in itemMap) { + return allCollections[itemMap[collectionId]]; + } + return undefined; + } + ); +} + +export default createCollectionSelector; diff --git a/frontend/src/Store/Selectors/createMovieClientSideCollectionItemsSelector.js b/frontend/src/Store/Selectors/createMovieClientSideCollectionItemsSelector.js index 755816c9d..336dd63d5 100644 --- a/frontend/src/Store/Selectors/createMovieClientSideCollectionItemsSelector.js +++ b/frontend/src/Store/Selectors/createMovieClientSideCollectionItemsSelector.js @@ -9,12 +9,14 @@ function createUnoptimizedSelector(uiSection) { const items = movies.items.map((s) => { const { id, - sortTitle + sortTitle, + collectionId } = s; return { id, - sortTitle + sortTitle, + collectionId }; }); diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js index 7b2e4d169..a486facba 100644 --- a/frontend/src/Store/Selectors/createProfileInUseSelector.js +++ b/frontend/src/Store/Selectors/createProfileInUseSelector.js @@ -7,12 +7,13 @@ function createProfileInUseSelector(profileProp) { (state, { id }) => id, createAllMoviesSelector(), (state) => state.settings.importLists.items, - (id, movies, lists) => { + (state) => state.movieCollections.items, + (id, movies, lists, collections) => { if (!id) { return false; } - if (_.some(movies, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) { + if (_.some(movies, { [profileProp]: id }) || _.some(lists, { [profileProp]: id }) || _.some(collections, { [profileProp]: id })) { return true; } diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js index 99a558b4d..74983dcbc 100644 --- a/frontend/src/Store/scrollPositions.js +++ b/frontend/src/Store/scrollPositions.js @@ -1,6 +1,7 @@ const scrollPositions = { movieIndex: 0, - discoverMovie: 0 + discoverMovie: 0, + movieCollections: 0 }; export default scrollPositions; diff --git a/frontend/src/Utilities/Movie/getNewMovie.js b/frontend/src/Utilities/Movie/getNewMovie.js index e9dc84489..387fae616 100644 --- a/frontend/src/Utilities/Movie/getNewMovie.js +++ b/frontend/src/Utilities/Movie/getNewMovie.js @@ -10,11 +10,12 @@ function getNewMovie(movie, payload) { } = payload; const addOptions = { + monitor, searchForMovie }; movie.addOptions = addOptions; - movie.monitored = monitor === 'true'; + movie.monitored = monitor !== 'none'; movie.qualityProfileId = qualityProfileId; movie.minimumAvailability = minimumAvailability; movie.rootFolderPath = rootFolderPath; diff --git a/frontend/src/Utilities/Movie/monitorOptions.js b/frontend/src/Utilities/Movie/monitorOptions.js new file mode 100644 index 000000000..db2c519b8 --- /dev/null +++ b/frontend/src/Utilities/Movie/monitorOptions.js @@ -0,0 +1,9 @@ +import translate from 'Utilities/String/translate'; + +const monitorOptions = [ + { key: 'movieOnly', value: translate('MovieOnly') }, + { key: 'movieAndCollection', value: translate('MovieAndCollection') }, + { key: 'none', value: translate('None') } +]; + +export default monitorOptions; diff --git a/package.json b/package.json index d2f91f2b9..c23bcced9 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "react-document-title": "2.0.3", "react-dom": "17.0.2", "react-focus-lock": "2.5.0", + "react-slick": "0.28.1", + "slick-carousel": "1.8.1", "react-google-recaptcha": "2.1.0", "react-lazyload": "3.2.0", "react-measure": "1.4.7", diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/208_collectionsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/208_collectionsFixture.cs new file mode 100644 index 000000000..9678b89cb --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/208_collectionsFixture.cs @@ -0,0 +1,293 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class collectionsFixture : MigrationTest + { + [Test] + public void should_add_collection_from_movie_and_link_back_to_movie() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Movies").Row(new + { + Monitored = true, + MinimumAvailability = 4, + ProfileId = 1, + MovieFileId = 0, + MovieMetadataId = 1, + Path = string.Format("/Movies/{0}", "Title"), + }); + + c.Insert.IntoTable("MovieMetadata").Row(new + { + Title = "Title", + CleanTitle = "CleanTitle", + Status = 3, + Images = new[] { new { CoverType = "Poster" } }.ToJson(), + Recommendations = new[] { 1 }.ToJson(), + Runtime = 90, + OriginalTitle = "Title", + CleanOriginalTitle = "CleanTitle", + OriginalLanguage = 1, + TmdbId = 132456, + Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(), + LastInfoSync = DateTime.UtcNow, + }); + }); + + var collections = db.Query("SELECT \"Id\", \"Title\", \"TmdbId\", \"Monitored\" FROM \"Collections\""); + + collections.Should().HaveCount(1); + collections.First().TmdbId.Should().Be(11); + collections.First().Title.Should().Be("Some Collection"); + collections.First().Monitored.Should().BeFalse(); + + var movies = db.Query("SELECT \"Id\", \"CollectionTmdbId\" FROM \"MovieMetadata\""); + + movies.Should().HaveCount(1); + movies.First().CollectionTmdbId.Should().Be(collections.First().TmdbId); + } + + [Test] + public void should_not_duplicate_collection() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Movies").Row(new + { + Monitored = true, + MinimumAvailability = 4, + ProfileId = 1, + MovieFileId = 0, + MovieMetadataId = 1, + Path = string.Format("/Movies/{0}", "Title"), + }); + + c.Insert.IntoTable("MovieMetadata").Row(new + { + Title = "Title", + CleanTitle = "CleanTitle", + Status = 3, + Images = new[] { new { CoverType = "Poster" } }.ToJson(), + Recommendations = new[] { 1 }.ToJson(), + Runtime = 90, + OriginalTitle = "Title", + CleanOriginalTitle = "CleanTitle", + OriginalLanguage = 1, + TmdbId = 132456, + Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(), + LastInfoSync = DateTime.UtcNow, + }); + + c.Insert.IntoTable("Movies").Row(new + { + Monitored = true, + MinimumAvailability = 4, + ProfileId = 1, + MovieFileId = 0, + MovieMetadataId = 2, + Path = string.Format("/Movies/{0}", "Title"), + }); + + c.Insert.IntoTable("MovieMetadata").Row(new + { + Title = "Title2", + CleanTitle = "CleanTitle2", + Status = 3, + Images = new[] { new { CoverType = "Poster" } }.ToJson(), + Recommendations = new[] { 1 }.ToJson(), + Runtime = 90, + OriginalTitle = "Title2", + CleanOriginalTitle = "CleanTitle2", + OriginalLanguage = 1, + TmdbId = 132457, + Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(), + LastInfoSync = DateTime.UtcNow, + }); + }); + + var collections = db.Query("SELECT \"Id\", \"Title\", \"TmdbId\", \"Monitored\" FROM \"Collections\""); + + collections.Should().HaveCount(1); + collections.First().TmdbId.Should().Be(11); + collections.First().Title.Should().Be("Some Collection"); + collections.First().Monitored.Should().BeFalse(); + } + + [Test] + public void should_migrate_true_monitor_setting_on_lists() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("ImportLists").Row(new + { + Enabled = true, + EnableAuto = true, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 4, + ShouldMonitor = true, + Name = "IMDB List", + Implementation = "RadarrLists", + Settings = new RadarrListSettings169 + { + APIURL = "https://api.radarr.video/v2", + Path = "/imdb/list?listId=ls000199717", + }.ToJson(), + ConfigContract = "RadarrSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Monitor\" FROM \"ImportLists\""); + + items.Should().HaveCount(1); + items.First().Monitor.Should().Be(0); + } + + [Test] + public void should_migrate_false_monitor_setting_on_lists() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("ImportLists").Row(new + { + Enabled = true, + EnableAuto = true, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 4, + ShouldMonitor = false, + Name = "IMDB List", + Implementation = "RadarrLists", + Settings = new RadarrListSettings169 + { + APIURL = "https://api.radarr.video/v2", + Path = "/imdb/list?listId=ls000199717", + }.ToJson(), + ConfigContract = "RadarrSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Monitor\" FROM \"ImportLists\""); + + items.Should().HaveCount(1); + items.First().Monitor.Should().Be(2); + } + + [Test] + public void should_purge_tmdb_collection_lists() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("ImportLists").Row(new + { + Enabled = true, + EnableAuto = true, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 4, + ShouldMonitor = false, + Name = "IMDB List", + Implementation = "TMDbCollectionImport", + Settings = new TmdbCollectionListSettings207 + { + CollectionId = "11" + }.ToJson(), + ConfigContract = "TMDbCollectionSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Monitor\" FROM \"ImportLists\""); + + items.Should().HaveCount(0); + } + + [Test] + public void should_monitor_new_collection_if_list_enabled_and_auto() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Movies").Row(new + { + Monitored = true, + MinimumAvailability = 4, + ProfileId = 1, + MovieFileId = 0, + MovieMetadataId = 1, + Path = string.Format("/Movies/{0}", "Title"), + }); + + c.Insert.IntoTable("MovieMetadata").Row(new + { + Title = "Title", + CleanTitle = "CleanTitle", + Status = 3, + Images = new[] { new { CoverType = "Poster" } }.ToJson(), + Recommendations = new[] { 1 }.ToJson(), + Runtime = 90, + OriginalTitle = "Title", + CleanOriginalTitle = "CleanTitle", + OriginalLanguage = 1, + TmdbId = 132456, + Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(), + LastInfoSync = DateTime.UtcNow, + }); + + c.Insert.IntoTable("ImportLists").Row(new + { + Enabled = true, + EnableAuto = true, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 4, + ShouldMonitor = false, + Name = "IMDB List", + Implementation = "TMDbCollectionImport", + Settings = new TmdbCollectionListSettings207 + { + CollectionId = "11" + }.ToJson(), + ConfigContract = "TMDbCollectionSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Monitored\" FROM \"Collections\""); + + items.Should().HaveCount(1); + items.First().Monitored.Should().BeTrue(); + } + } + + public class Collection208 + { + public int Id { get; set; } + public int TmdbId { get; set; } + public string Title { get; set; } + public bool Monitored { get; set; } + } + + public class Movie208 + { + public int Id { get; set; } + public int CollectionTmdbId { get; set; } + } + + public class ListDefinition208 + { + public int Id { get; set; } + public int Monitor { get; set; } + } + + public class TmdbCollectionListSettings207 + { + public string CollectionId { get; set; } + } +} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedCollectionsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedCollectionsFixture.cs new file mode 100644 index 000000000..79ffe3a84 --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedCollectionsFixture.cs @@ -0,0 +1,47 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupOrphanedCollectionsFixture : DbTest + { + [Test] + public void should_delete_orphaned_collection_item() + { + var collection = Builder.CreateNew() + .With(h => h.Id = 3) + .With(h => h.TmdbId = 123456) + .With(h => h.Title = "Some Credit") + .BuildNew(); + + Db.Insert(collection); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_unorphaned_collection_items() + { + var collection = Builder.CreateNew() + .With(h => h.Id = 3) + .With(h => h.TmdbId = 123456) + .With(h => h.Title = "Some Credit") + .BuildNew(); + + Db.Insert(collection); + + var movie = Builder.CreateNew().With(m => m.CollectionTmdbId = collection.TmdbId).BuildNew(); + + Db.Insert(movie); + + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/MovieTests/AddMovieFixture.cs b/src/NzbDrone.Core.Test/MovieTests/AddMovieFixture.cs index 1652da06c..a97d6196b 100644 --- a/src/NzbDrone.Core.Test/MovieTests/AddMovieFixture.cs +++ b/src/NzbDrone.Core.Test/MovieTests/AddMovieFixture.cs @@ -27,6 +27,8 @@ namespace NzbDrone.Core.Test.MovieTests { _fakeMovie = Builder .CreateNew() + .With(x => x.CollectionTitle = null) + .With(x => x.CollectionTmdbId = 0) .Build(); } diff --git a/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs b/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs index 8acb3706c..d1ee7fa67 100644 --- a/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Commands; using NzbDrone.Core.Movies.Credits; using NzbDrone.Core.Test.Framework; @@ -19,6 +20,7 @@ namespace NzbDrone.Core.Test.MovieTests public class RefreshMovieServiceFixture : CoreTest { private MovieMetadata _movie; + private MovieCollection _movieCollection; private Movie _existingMovie; [SetUp] @@ -28,6 +30,9 @@ namespace NzbDrone.Core.Test.MovieTests .With(s => s.Status = MovieStatusType.Released) .Build(); + _movieCollection = Builder.CreateNew() + .Build(); + _existingMovie = Builder.CreateNew() .With(s => s.MovieMetadata.Value.Status = MovieStatusType.Released) .Build(); @@ -40,6 +45,10 @@ namespace NzbDrone.Core.Test.MovieTests .Setup(s => s.Get(_movie.Id)) .Returns(_movie); + Mocker.GetMock() + .Setup(v => v.AddMovieCollection(It.IsAny())) + .Returns(_movieCollection); + Mocker.GetMock() .Setup(s => s.GetMovieInfo(It.IsAny())) .Callback((i) => { throw new MovieNotFoundException(i); }); diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 3ac3761ce..42bd3a643 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Translations; using NzbDrone.Core.Organizer; using NzbDrone.Core.Qualities; @@ -226,7 +227,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests public void should_replace_movie_collection() { _namingConfig.StandardMovieFormat = "{Movie Collection}"; - _movie.MovieMetadata.Value.Collection = new MovieCollection { Name = "South Part Collection" }; + _movie.MovieMetadata.Value.CollectionTitle = "South Part Collection"; Subject.BuildFileName(_movie, _movieFile) .Should().Be("South Part Collection"); diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs index b4739718b..583730671 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.ImportLists; using NzbDrone.Core.Languages; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Profiles; using NzbDrone.Core.Test.CustomFormats; using NzbDrone.Core.Test.Framework; @@ -90,6 +91,33 @@ namespace NzbDrone.Core.Test.Profiles Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); } + [Test] + public void should_not_be_able_to_delete_profile_if_assigned_to_collection() + { + var movieList = Builder.CreateListOfSize(3) + .All() + .With(c => c.ProfileId = 1) + .Build().ToList(); + + var importList = Builder.CreateListOfSize(3) + .Random(1) + .With(c => c.ProfileId = 1) + .Build().ToList(); + + var collectionList = Builder.CreateListOfSize(3) + .All() + .With(c => c.QualityProfileId = 2) + .Build().ToList(); + + Mocker.GetMock().Setup(c => c.GetAllMovies()).Returns(movieList); + Mocker.GetMock().Setup(c => c.All()).Returns(importList); + Mocker.GetMock().Setup(c => c.GetAllCollections()).Returns(collectionList); + + Assert.Throws(() => Subject.Delete(2)); + + Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); + } + [Test] public void should_delete_profile_if_not_assigned_to_movie_or_list() { @@ -103,8 +131,14 @@ namespace NzbDrone.Core.Test.Profiles .With(c => c.ProfileId = 2) .Build().ToList(); + var collectionList = Builder.CreateListOfSize(3) + .All() + .With(c => c.QualityProfileId = 2) + .Build().ToList(); + Mocker.GetMock().Setup(c => c.GetAllMovies()).Returns(movieList); Mocker.GetMock().Setup(c => c.All()).Returns(importList); + Mocker.GetMock().Setup(c => c.GetAllCollections()).Returns(collectionList); Subject.Delete(1); diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 8dc2b746e..277b60a16 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -5,7 +5,6 @@ using System.Linq; using NLog; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Http.Proxy; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index dade3eef1..c51b27722 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using NzbDrone.Common.Http.Proxy; -using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Qualities; diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs index 29683ebd5..7aac93c54 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs index e031dd007..af3202c9f 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.CustomFormats diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs index 532087b6c..1124783fb 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Datastore/LazyLoaded.cs b/src/NzbDrone.Core/Datastore/LazyLoaded.cs index 91ff44c82..288726efb 100644 --- a/src/NzbDrone.Core/Datastore/LazyLoaded.cs +++ b/src/NzbDrone.Core/Datastore/LazyLoaded.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Text.Json.Serialization; using NLog; using NzbDrone.Common.Instrumentation; @@ -15,6 +16,7 @@ namespace NzbDrone.Core.Datastore /// Allows a field to be lazy loaded. /// /// + [JsonConverter(typeof(LazyLoadedConverterFactory))] public class LazyLoaded : ILazyLoaded { protected TChild _value; @@ -62,11 +64,6 @@ namespace NzbDrone.Core.Datastore { return MemberwiseClone(); } - - public bool ShouldSerializeValue() - { - return IsLoaded; - } } /// diff --git a/src/NzbDrone.Core/Datastore/LazyLoadedConverterFactory.cs b/src/NzbDrone.Core/Datastore/LazyLoadedConverterFactory.cs new file mode 100644 index 000000000..4e02ef794 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/LazyLoadedConverterFactory.cs @@ -0,0 +1,90 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NzbDrone.Core.Datastore +{ + public class LazyLoadedConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + { + return false; + } + + return typeToConvert.GetGenericTypeDefinition() == typeof(LazyLoaded<>); + } + + public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) + { + var childType = type.GetGenericArguments()[0]; + + return (JsonConverter)Activator.CreateInstance( + typeof(LazyLoadedConverter<>).MakeGenericType(childType), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: new object[] { options }, + culture: null); + } + + private class LazyLoadedConverter : JsonConverter> + { + private readonly JsonConverter _childConverter; + private readonly Type _childType; + + public LazyLoadedConverter(JsonSerializerOptions options) + { + // For performance, use the existing converter if available. + _childConverter = (JsonConverter)options + .GetConverter(typeof(TChild)); + + // Cache the type. + _childType = typeof(TChild); + } + + public override LazyLoaded Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + TChild value; + if (_childConverter != null) + { + reader.Read(); + value = _childConverter.Read(ref reader, _childType, options); + } + else + { + value = JsonSerializer.Deserialize(ref reader, options); + } + + if (value != null) + { + return new LazyLoaded(value); + } + else + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, LazyLoaded value, JsonSerializerOptions options) + { + if (value.IsLoaded) + { + if (_childConverter != null) + { + _childConverter.Write(writer, value.Value, options); + } + else + { + JsonSerializer.Serialize(writer, value.Value, options); + } + } + else + { + writer.WriteNullValue(); + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/165_remove_custom_formats_from_quality_model.cs b/src/NzbDrone.Core/Datastore/Migration/165_remove_custom_formats_from_quality_model.cs index 236aac66e..4dc4a2a13 100644 --- a/src/NzbDrone.Core/Datastore/Migration/165_remove_custom_formats_from_quality_model.cs +++ b/src/NzbDrone.Core/Datastore/Migration/165_remove_custom_formats_from_quality_model.cs @@ -4,7 +4,6 @@ using System.Data; using System.Linq; using Dapper; using FluentMigrator; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Datastore.Migration.Framework; diff --git a/src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs b/src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs index 111a7e1c9..cc70f0e3a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs +++ b/src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Data; using FluentMigrator; using NzbDrone.Core.Datastore.Migration.Framework; diff --git a/src/NzbDrone.Core/Datastore/Migration/190_update_awesome_hd_link.cs b/src/NzbDrone.Core/Datastore/Migration/190_update_awesome_hd_link.cs index 42029d793..d1c228488 100644 --- a/src/NzbDrone.Core/Datastore/Migration/190_update_awesome_hd_link.cs +++ b/src/NzbDrone.Core/Datastore/Migration/190_update_awesome_hd_link.cs @@ -1,5 +1,4 @@ using FluentMigrator; -using Newtonsoft.Json.Linq; using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration diff --git a/src/NzbDrone.Core/Datastore/Migration/195_update_notifiarr.cs b/src/NzbDrone.Core/Datastore/Migration/195_update_notifiarr.cs index a8cccfd6a..a773cedce 100644 --- a/src/NzbDrone.Core/Datastore/Migration/195_update_notifiarr.cs +++ b/src/NzbDrone.Core/Datastore/Migration/195_update_notifiarr.cs @@ -1,5 +1,4 @@ using FluentMigrator; -using Newtonsoft.Json.Linq; using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration diff --git a/src/NzbDrone.Core/Datastore/Migration/200_cdh_per_downloadclient.cs b/src/NzbDrone.Core/Datastore/Migration/200_cdh_per_downloadclient.cs index 68bbdd70a..cf80cc465 100644 --- a/src/NzbDrone.Core/Datastore/Migration/200_cdh_per_downloadclient.cs +++ b/src/NzbDrone.Core/Datastore/Migration/200_cdh_per_downloadclient.cs @@ -1,8 +1,5 @@ using System.Data; -using System.Linq; using FluentMigrator; -using Newtonsoft.Json.Linq; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration diff --git a/src/NzbDrone.Core/Datastore/Migration/208_collections.cs b/src/NzbDrone.Core/Datastore/Migration/208_collections.cs new file mode 100644 index 000000000..9d5f8cd05 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/208_collections.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Dapper; +using FluentMigrator; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(208)] + public class collections : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("Collections") + .WithColumn("TmdbId").AsInt32().Unique() + .WithColumn("QualityProfileId").AsInt32() + .WithColumn("RootFolderPath").AsString() + .WithColumn("MinimumAvailability").AsInt32() + .WithColumn("SearchOnAdd").AsBoolean() + .WithColumn("Title").AsString() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("CleanTitle").AsString() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Images").AsString().WithDefaultValue("[]") + .WithColumn("Monitored").AsBoolean().WithDefaultValue(false) + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("Added").AsDateTime().Nullable(); + + Alter.Table("MovieMetadata").AddColumn("CollectionTmdbId").AsInt32().Nullable() + .AddColumn("CollectionTitle").AsString().Nullable(); + + Alter.Table("ImportLists").AddColumn("Monitor").AsInt32().Nullable(); + + Execute.WithConnection(MigrateCollections); + Execute.WithConnection(MigrateCollectionMonitorStatus); + Execute.WithConnection(MapCollections); + Execute.WithConnection(MigrateListMonitor); + + Alter.Table("ImportLists").AlterColumn("Monitor").AsInt32().NotNullable(); + + Delete.Column("ShouldMonitor").FromTable("ImportLists"); + Delete.FromTable("ImportLists").Row(new { Implementation = "TMDbCollectionImport" }); + Delete.Column("Collection").FromTable("MovieMetadata"); + } + + private void MigrateCollections(IDbConnection conn, IDbTransaction tran) + { + var rootPaths = new List(); + using (var getRootFolders = conn.CreateCommand()) + { + getRootFolders.Transaction = tran; + getRootFolders.CommandText = @"SELECT ""Path"" FROM ""RootFolders"""; + + using (var definitionsReader = getRootFolders.ExecuteReader()) + { + while (definitionsReader.Read()) + { + string path = definitionsReader.GetString(0); + rootPaths.Add(path); + } + } + } + + var newCollections = new List(); + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Collection\", \"ProfileId\", \"MinimumAvailability\", \"Path\" FROM \"Movies\" JOIN \"MovieMetadata\" ON \"Movies\".\"MovieMetadataId\" = \"MovieMetadata\".\"Id\" WHERE \"Collection\" IS NOT NULL"; + + var addedCollections = new List(); + var added = DateTime.UtcNow; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var collection = reader.GetString(0); + var qualityProfileId = reader.GetInt32(1); + var minimumAvailability = reader.GetInt32(2); + var moviePath = reader.GetString(3); + var data = STJson.Deserialize(collection); + + if (newCollections.Any(d => d.TmdbId == data.TmdbId)) + { + continue; + } + + var rootFolderPath = rootPaths.Where(r => r.IsParentPath(moviePath)) + .OrderByDescending(r => r.Length) + .FirstOrDefault(); + + if (rootFolderPath == null) + { + rootFolderPath = rootPaths.FirstOrDefault(); + } + + if (rootFolderPath == null) + { + rootFolderPath = moviePath.GetParentPath(); + } + + newCollections.Add(new MovieCollection208 + { + TmdbId = data.TmdbId, + Title = data.Name, + CleanTitle = data.Name.CleanMovieTitle(), + SortTitle = Parser.Parser.NormalizeTitle(data.Name), + Added = added, + QualityProfileId = qualityProfileId, + RootFolderPath = rootFolderPath, + SearchOnAdd = true, + MinimumAvailability = minimumAvailability + }); + } + } + } + + var updateSql = "INSERT INTO \"Collections\" (\"TmdbId\", \"Title\", \"CleanTitle\", \"SortTitle\", \"Added\", \"QualityProfileId\", \"RootFolderPath\", \"SearchOnAdd\", \"MinimumAvailability\") VALUES (@TmdbId, @Title, @CleanTitle, @SortTitle, @Added, @QualityProfileId, @RootFolderPath, @SearchOnAdd, @MinimumAvailability)"; + conn.Execute(updateSql, newCollections, transaction: tran); + } + + private void MigrateCollectionMonitorStatus(IDbConnection conn, IDbTransaction tran) + { + var updatedCollections = new List(); + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Enabled\", \"EnableAuto\", \"Settings\", \"ShouldMonitor\", \"Id\" FROM \"ImportLists\" WHERE \"Implementation\" = 'TMDbCollectionImport'"; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var enabled = reader.GetBoolean(0); + var enabledAutoAdd = reader.GetBoolean(1); + var settings = reader.GetString(2); + var shouldMonitor = reader.GetBoolean(3); + var listId = reader.GetInt32(4); + var data = STJson.Deserialize(settings); + + if (!enabled || !enabledAutoAdd || !int.TryParse(data.CollectionId, out var collectionId)) + { + continue; + } + + updatedCollections.Add(new MovieCollection208 + { + TmdbId = collectionId, + Monitored = true + }); + } + } + } + + var updateSql = "UPDATE \"Collections\" SET \"Monitored\" = @Monitored WHERE \"TmdbId\" = @TmdbId"; + conn.Execute(updateSql, updatedCollections, transaction: tran); + } + + private void MigrateListMonitor(IDbConnection conn, IDbTransaction tran) + { + var updatedLists = new List(); + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"ShouldMonitor\", \"Id\" FROM \"ImportLists\""; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var shouldMonitor = reader.GetBoolean(0); + var listId = reader.GetInt32(1); + + updatedLists.Add(new ImportList208 + { + Monitor = shouldMonitor ? 0 : 2, + Id = listId + }); + } + } + } + + var updateSql = "UPDATE \"ImportLists\" SET \"Monitor\" = @Monitor WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updatedLists, transaction: tran); + } + + private void MapCollections(IDbConnection conn, IDbTransaction tran) + { + var updatedMeta = new List(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"Collection\" FROM \"MovieMetadata\" WHERE \"Collection\" IS NOT NULL"; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var collection = reader.GetString(1); + var data = STJson.Deserialize(collection); + + var collectionId = data.TmdbId; + var collectionTitle = data.Name; + + updatedMeta.Add(new MovieMetadata208 + { + CollectionTitle = collectionTitle, + CollectionTmdbId = collectionId, + Id = id + }); + } + } + } + + var updateSql = "UPDATE \"MovieMetadata\" SET \"CollectionTmdbId\" = @CollectionTmdbId, \"CollectionTitle\" = @CollectionTitle WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updatedMeta, transaction: tran); + } + + private class MovieCollection207 + { + public string Name { get; set; } + public int TmdbId { get; set; } + } + + private class MovieCollection208 + { + public int Id { get; set; } + public string Title { get; set; } + public string CleanTitle { get; set; } + public string SortTitle { get; set; } + public DateTime Added { get; set; } + public int QualityProfileId { get; set; } + public string RootFolderPath { get; set; } + public bool SearchOnAdd { get; set; } + public int MinimumAvailability { get; set; } + public bool Monitored { get; set; } + public int TmdbId { get; set; } + } + + private class MovieMetadata208 + { + public int Id { get; set; } + public int CollectionTmdbId { get; set; } + public string CollectionTitle { get; set; } + } + + private class ImportList208 + { + public int Id { get; set; } + public int Monitor { get; set; } + } + + private class TmdbCollectionSettings206 + { + public string CollectionId { get; set; } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index cc609e986..357ce19c5 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -28,6 +28,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Movies; using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Credits; using NzbDrone.Core.Movies.Translations; using NzbDrone.Core.Notifications; @@ -168,6 +169,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("MovieMetadata").RegisterModel() .Ignore(s => s.Translations); + + Mapper.Entity("Collections").RegisterModel(); } private static void RegisterMappers() diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeLabel.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeLabel.cs index a5da831ab..3d95413c5 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeLabel.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeLabel.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Deluge diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStation2Task.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStation2Task.cs index f7d9ab927..71e2ac11a 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStation2Task.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStation2Task.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using NzbDrone.Common.Serializer; - namespace NzbDrone.Core.Download.Clients.DownloadStation { public class DownloadStation2Task diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxySelector.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxySelector.cs index 3991e357b..1eae7930e 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxySelector.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using NLog; using NzbDrone.Common.Cache; -using NzbDrone.Common.Http; namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies { diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index 3ae3fa7b1..33fe7a052 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Download.Clients.Flood switch (additionalTag) { case (int)AdditionalTags.Collection: - result.Add(remoteMovie.Movie.MovieMetadata.Value.Collection.Name); + result.Add(remoteMovie.Movie.MovieMetadata.Value.CollectionTitle); break; case (int)AdditionalTags.Quality: result.Add(remoteMovie.ParsedMovieInfo.Quality.Quality.ToString()); diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 76e729592..4819aef33 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; diff --git a/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs b/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs index 1e9deec9f..bce66c8bc 100644 --- a/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs +++ b/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Xml.Linq; using System.Xml.XPath; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 6f427e232..e64f5b02d 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -251,11 +251,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc details.Add(new XElement("country")); - if (movie.MovieMetadata.Value.Collection?.Name != null) + if (movie.MovieMetadata.Value.CollectionTitle != null) { var setElement = new XElement("set"); - setElement.Add(new XElement("name", movie.MovieMetadata.Value.Collection.Name)); + setElement.Add(new XElement("name", movie.MovieMetadata.Value.CollectionTitle)); setElement.Add(new XElement("overview")); details.Add(setElement); diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs index fad65a348..dc07e4d4f 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs index 2bc24a5a8..3b41b929b 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs @@ -1,6 +1,5 @@ using System.Linq; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Localization; using NzbDrone.Core.Movies; diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MovieCollectionRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MovieCollectionRootFolderCheck.cs new file mode 100644 index 000000000..60f749488 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/MovieCollectionRootFolderCheck.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Localization; +using NzbDrone.Core.Movies.Collections; +using NzbDrone.Core.Movies.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(CollectionEditedEvent), CheckOnCondition.Always)] + public class MovieCollectionRootFolderCheck : HealthCheckBase + { + private readonly IMovieCollectionService _collectionService; + private readonly IDiskProvider _diskProvider; + + public MovieCollectionRootFolderCheck(IMovieCollectionService collectionService, IDiskProvider diskProvider, ILocalizationService localizationService) + : base(localizationService) + { + _collectionService = collectionService; + _diskProvider = diskProvider; + } + + public override HealthCheck Check() + { + var collections = _collectionService.GetAllCollections(); + var missingRootFolders = new Dictionary>(); + + foreach (var collection in collections) + { + var rootFolderPath = collection.RootFolderPath; + + if (missingRootFolders.ContainsKey(rootFolderPath)) + { + missingRootFolders[rootFolderPath].Add(collection); + + continue; + } + + if (rootFolderPath.IsNullOrWhiteSpace() || !_diskProvider.FolderExists(rootFolderPath)) + { + missingRootFolders.Add(rootFolderPath, new List { collection }); + } + } + + if (missingRootFolders.Any()) + { + if (missingRootFolders.Count == 1) + { + var missingRootFolder = missingRootFolders.First(); + return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("MovieCollectionMissingRoot"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), "#movie-collection-missing-root-folder"); + } + + var message = string.Format(_localizationService.GetLocalizedString("MovieCollectionMultipleMissingRoots"), string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value)))); + return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#movie-collection-missing-root-folder"); + } + + return new HealthCheck(GetType()); + } + + private string FormatRootFolder(string rootFolderPath, List collections) + { + return $"{rootFolderPath} ({string.Join(", ", collections.Select(l => l.Title))})"; + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/PackageGlobalMessageCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/PackageGlobalMessageCheck.cs index 98d40734b..9299088aa 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/PackageGlobalMessageCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/PackageGlobalMessageCheck.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedCollections.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedCollections.cs new file mode 100644 index 000000000..233a8d9db --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedCollections.cs @@ -0,0 +1,28 @@ +using Dapper; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupOrphanedCollections : IHousekeepingTask + { + private readonly IMainDatabase _database; + + public CleanupOrphanedCollections(IMainDatabase database) + { + _database = database; + } + + public void Clean() + { + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM ""Collections"" + WHERE ""TmdbId"" IN ( + SELECT ""Collections"".""TmdbId"" FROM ""Collections"" + LEFT OUTER JOIN ""MovieMetadata"" + ON ""Collections"".""TmdbId"" = ""MovieMetadata"".""CollectionTmdbId"" + WHERE ""MovieMetadata"".""Id"" IS NULL)"); + } + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMovieMetadata.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMovieMetadata.cs index e7e10bc40..aad19ad07 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMovieMetadata.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMovieMetadata.cs @@ -20,8 +20,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE ""Id"" IN ( SELECT ""MovieMetadata"".""Id"" FROM ""MovieMetadata"" LEFT OUTER JOIN ""Movies"" ON ""Movies"".""MovieMetadataId"" = ""MovieMetadata"".""Id"" + LEFT OUTER JOIN ""Collections"" ON ""Collections"".""TmdbId"" = ""MovieMetadata"".""CollectionTmdbId"" LEFT OUTER JOIN ""ImportListMovies"" ON ""ImportListMovies"".""MovieMetadataId"" = ""MovieMetadata"".""Id"" - WHERE ""Movies"".""Id"" IS NULL AND ""ImportListMovies"".""Id"" IS NULL)"); + WHERE ""Movies"".""Id"" IS NULL AND ""ImportListMovies"".""Id"" IS NULL AND ""Collections"".""Id"" IS NULL)"); } } } diff --git a/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs b/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs index 3f0d24175..a2ac8b020 100644 --- a/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs +++ b/src/NzbDrone.Core/ImportLists/FetchAndParseImportListService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.TPL; using NzbDrone.Core.ImportLists.ImportListMovies; diff --git a/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs b/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs index 97fadae36..0a8c0eeef 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.ImportLists public bool Enabled { get; set; } public bool EnableAuto { get; set; } - public bool ShouldMonitor { get; set; } + public MonitorTypes Monitor { get; set; } public MovieStatusType MinimumAvailability { get; set; } public int ProfileId { get; set; } public string RootFolderPath { get; set; } diff --git a/src/NzbDrone.Core/ImportLists/ImportListFactory.cs b/src/NzbDrone.Core/ImportLists/ImportListFactory.cs index 14b17bb4f..409680b8a 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListFactory.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListFactory.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 7fb5a01ee..204db5b89 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -91,11 +91,11 @@ namespace NzbDrone.Core.ImportLists // Append Artist if not already in DB or already on add list if (moviesToAdd.All(s => s.TmdbId != report.TmdbId)) { - var monitored = importList.ShouldMonitor; + var monitorType = importList.Monitor; moviesToAdd.Add(new Movie { - Monitored = monitored, + Monitored = monitorType != MonitorTypes.None, RootFolderPath = importList.RootFolderPath, ProfileId = importList.ProfileId, MinimumAvailability = importList.MinimumAvailability, @@ -106,7 +106,8 @@ namespace NzbDrone.Core.ImportLists ImdbId = report.ImdbId, AddOptions = new AddMovieOptions { - SearchForMovie = monitored && importList.SearchOnAdd, + SearchForMovie = monitorType != MonitorTypes.None && importList.SearchOnAdd, + Monitor = monitorType } }); } diff --git a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionImport.cs b/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionImport.cs deleted file mode 100644 index a7f691c13..000000000 --- a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionImport.cs +++ /dev/null @@ -1,43 +0,0 @@ -using NLog; -using NzbDrone.Common.Cloud; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.ImportLists.TMDb.Collection -{ - public class TMDbCollectionImport : TMDbImportListBase - { - public TMDbCollectionImport(IRadarrCloudRequestBuilder requestBuilder, - IHttpClient httpClient, - IImportListStatusService importListStatusService, - IConfigService configService, - IParsingService parsingService, - ISearchForNewMovie searchForNewMovie, - Logger logger) - : base(requestBuilder, httpClient, importListStatusService, configService, parsingService, searchForNewMovie, logger) - { - } - - public override string Name => "TMDb Collection"; - public override bool Enabled => true; - public override bool EnableAuto => false; - - public override IParseImportListResponse GetParser() - { - return new TMDbCollectionParser(); - } - - public override IImportListRequestGenerator GetRequestGenerator() - { - return new TMDbCollectionRequestGenerator() - { - RequestBuilder = _requestBuilder, - Settings = Settings, - Logger = _logger, - HttpClient = _httpClient - }; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionParser.cs b/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionParser.cs deleted file mode 100644 index b97f29e7c..000000000 --- a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionParser.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.ImportLists.ImportListMovies; - -namespace NzbDrone.Core.ImportLists.TMDb.Collection -{ - public class TMDbCollectionParser : TMDbParser - { - public override IList ParseResponse(ImportListResponse importResponse) - { - var movies = new List(); - - if (!PreProcess(importResponse)) - { - return movies; - } - - var jsonResponse = JsonConvert.DeserializeObject(importResponse.Content); - - // no movies were return - if (jsonResponse == null) - { - return movies; - } - - foreach (var movie in jsonResponse.Parts) - { - // Movies with no Year Fix - if (string.IsNullOrWhiteSpace(movie.ReleaseDate)) - { - continue; - } - - movies.AddIfNotNull(MapListMovie(movie)); - } - - return movies; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionRequestGenerator.cs deleted file mode 100644 index 10ac3342a..000000000 --- a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionRequestGenerator.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using NLog; -using NzbDrone.Common.Http; - -namespace NzbDrone.Core.ImportLists.TMDb.Collection -{ - public class TMDbCollectionRequestGenerator : IImportListRequestGenerator - { - public TMDbCollectionSettings Settings { get; set; } - public IHttpClient HttpClient { get; set; } - public IHttpRequestBuilderFactory RequestBuilder { get; set; } - public Logger Logger { get; set; } - - public TMDbCollectionRequestGenerator() - { - } - - public virtual ImportListPageableRequestChain GetMovies() - { - var pageableRequests = new ImportListPageableRequestChain(); - - pageableRequests.Add(GetMoviesRequest()); - - return pageableRequests; - } - - private IEnumerable GetMoviesRequest() - { - Logger.Info($"Importing TMDb movies from collection: {Settings.CollectionId}"); - - yield return new ImportListRequest(RequestBuilder.Create() - .SetSegment("api", "3") - .SetSegment("route", "collection") - .SetSegment("id", Settings.CollectionId) - .SetSegment("secondaryRoute", "") - .Build()); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionSettings.cs b/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionSettings.cs deleted file mode 100644 index 59200a39f..000000000 --- a/src/NzbDrone.Core/ImportLists/TMDb/Collection/TMDbCollectionSettings.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text.RegularExpressions; -using FluentValidation; -using NzbDrone.Core.Annotations; - -namespace NzbDrone.Core.ImportLists.TMDb.Collection -{ - public class TMDbCollectionSettingsValidator : TMDbSettingsBaseValidator - { - public TMDbCollectionSettingsValidator() - : base() - { - RuleFor(c => c.CollectionId).Matches(@"^[1-9][0-9]*$", RegexOptions.IgnoreCase); - } - } - - public class TMDbCollectionSettings : TMDbSettingsBase - { - protected override AbstractValidator Validator => new TMDbCollectionSettingsValidator(); - - public TMDbCollectionSettings() - { - CollectionId = ""; - } - - [FieldDefinition(1, Label = "Collection Id", Type = FieldType.Textbox, HelpText = "TMDb Id of Collection to Follow")] - public string CollectionId { get; set; } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs index 591c852a3..78b4b6885 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Net.Http; -using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Trakt; namespace NzbDrone.Core.ImportLists.Trakt.List diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs index 59004deee..82f1cf613 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Net.Http; -using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Trakt; namespace NzbDrone.Core.ImportLists.Trakt.Popular diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs index bc058e34b..24017d23f 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Net.Http; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Trakt; namespace NzbDrone.Core.ImportLists.Trakt.User diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 11b489245..d71f6fc87 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index 08b260609..4e72a36cd 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using NLog; -using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs index 1111a1c41..6a54424e9 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Linq; using System.Xml.Linq; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 28e83817e..fab9e4b0b 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -101,6 +101,12 @@ namespace NzbDrone.Core.Jobs TypeName = typeof(CleanUpRecycleBinCommand).FullName }, + new ScheduledTask + { + Interval = 24 * 60, + TypeName = typeof(RefreshCollectionsCommand).FullName + }, + new ScheduledTask { Interval = GetBackupInterval(), diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index fa7aa182b..bde6e042e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -17,7 +17,6 @@ "AddListExclusion": "Add List Exclusion", "AddMovie": "Add Movie", "AddMovies": "Add Movies", - "AddMoviesMonitored": "Add Movies Monitored", "AddNew": "Add New", "AddNewMessage": "It's easy to add a new movie, just start typing the name of the movie you want to add", "AddNewMovie": "Add New Movie", @@ -33,6 +32,7 @@ "Agenda": "Agenda", "AgeWhenGrabbed": "Age (when grabbed)", "All": "All", + "AllCollectionsHiddenDueToFilter": "All collections are hidden due to applied filter.", "AllFiles": "All Files", "AllMoviesHiddenDueToFilter": "All movies are hidden due to applied filter.", "AllMoviesInPathHaveBeenImported": "All movies in {0} have been imported", @@ -137,6 +137,8 @@ "Close": "Close", "CloseCurrentModal": "Close Current Modal", "Collection": "Collection", + "Collections": "Collections", + "CollectionsSelectedInterp": "{0} Collections(s) Selected", "ColonReplacement": "Colon Replacement", "ColonReplacementFormatHelpText": "Change how Radarr handles colon replacement", "Columns": "Columns", @@ -266,6 +268,7 @@ "DownloadWarningCheckDownloadClientForMoreDetails": "Download warning: check download client for more details", "Duration": "Duration", "Edit": "Edit", + "EditCollection": "Edit Collection", "EditCustomFormat": "Edit Custom Format", "EditDelayProfile": "Edit Delay Profile", "EditGroups": "Edit Groups", @@ -537,11 +540,14 @@ "Mode": "Mode", "Monday": "Monday", "Monitor": "Monitor", + "MonitorCollection": "Monitor Collection", "Monitored": "Monitored", + "MonitoredCollectionHelpText": "Monitor to automatically have movies from this collection added to the library", "MonitoredHelpText": "Download movie if available", "MonitoredOnly": "Monitored Only", "MonitoredStatus": "Monitored/Status", "MonitorMovie": "Monitor Movie", + "MonitorMovies": "Monitor Movies", "Month": "Month", "Months": "Months", "More": "More", @@ -554,7 +560,10 @@ "MoveFolders2": "Would you like to move the movie files from '{0}' to '{1}' ?", "Movie": "Movie", "MovieAlreadyExcluded": "Movie already Excluded", + "MovieAndCollection": "Movie and Collection", "MovieChat": "Movie Chat", + "MovieCollectionMissingRoot": "Missing root folder for movie collection: {0}", + "MovieCollectionMultipleMissingRoots": "Multiple root folders are missing for movie collections: {0}", "MovieDetailsNextMovie": "Movie Details: Next Movie", "MovieDetailsPreviousMovie": "Movie Details: Previous Movie", "MovieEditor": "Movie Editor", @@ -577,6 +586,7 @@ "MovieIsRecommend": "Movie is recommended based on recent addition", "MovieIsUnmonitored": "Movie is unmonitored", "MovieNaming": "Movie Naming", + "MovieOnly": "Movie Only", "Movies": "Movies", "MoviesSelectedInterp": "{0} Movie(s) Selected", "MovieTitle": "Movie Title", @@ -595,11 +605,11 @@ "Never": "Never", "New": "New", "NextExecution": "Next Execution", - "No": "No", "NoAltTitle": "No alternative titles.", "NoBackupsAreAvailable": "No backups are available", "NoChange": "No Change", "NoChanges": "No Changes", + "NoCollections": "No collections found, to get started you'll want to add a new movie, or import some existing ones", "NoEventsFound": "No events found", "NoHistory": "No history", "NoLeaveIt": "No, Leave It", @@ -749,6 +759,7 @@ "Redownload": "Redownload", "Refresh": "Refresh", "RefreshAndScan": "Refresh & Scan", + "RefreshCollections": "Refresh Collections", "RefreshInformationAndScanDisk": "Refresh information and scan disk", "RefreshLists": "Refresh Lists", "RefreshMonitoredIntervalHelpText": "How often to refresh monitored downloads from download clients, minimum 1 minute", @@ -855,6 +866,7 @@ "Score": "Score", "Script": "Script", "ScriptPath": "Script Path", + "ScrollMovies": "Scroll Movies", "Search": "Search", "SearchAll": "Search All", "SearchCutoffUnmet": "Search Cutoff Unmet", @@ -865,7 +877,8 @@ "SearchMissing": "Search Missing", "SearchMovie": "Search Movie", "SearchOnAdd": "Search on Add", - "SearchOnAddHelpText": "Search for movies on this list when added to Radarr", + "SearchOnAddCollectionHelpText": "Search for movies on this collection when added to library", + "SearchOnAddHelpText": "Search for movies on this list when added to library", "SearchSelected": "Search Selected", "Seconds": "Seconds", "Security": "Security", @@ -901,12 +914,13 @@ "SettingsTimeFormat": "Time Format", "SettingsWeekColumnHeader": "Week Column Header", "SettingsWeekColumnHeaderHelpText": "Shown above each column when week is the active view", - "ShouldMonitorHelpText": "If enabled, movies added by this list are added and monitored", + "ShouldMonitorHelpText": "Should Movies or Collections added by this list be added as monitored", "ShowAdvanced": "Show Advanced", "ShowAsAllDayEvents": "Show as All-Day Events", "ShowCertification": "Show Certification", "ShowCinemaRelease": "Show Cinema Release Date", "showCinemaReleaseHelpText": "Show cinema release date under poster", + "ShowCollectionDetails": "Show Collection Status", "ShowCutoffUnmetIconHelpText": "Show icon for files when the cutoff hasn't been met", "ShowDateAdded": "Show Date Added", "ShowGenres": "Show Genres", @@ -915,6 +929,7 @@ "ShowMovieInformation": "Show Movie Information", "ShowMovieInformationHelpText": "Show movie genres and certification", "ShownClickToHide": "Shown, click to hide", + "ShowOverview": "Show Overview", "ShowPath": "Show Path", "ShowQualityProfile": "Show Quality Profile", "ShowQualityProfileHelpText": "Show quality profile under poster", @@ -1029,6 +1044,7 @@ "UnableToLoadAltTitle": "Unable to load alternative titles.", "UnableToLoadBackups": "Unable to load backups", "UnableToLoadBlocklist": "Unable to load blocklist", + "UnableToLoadCollections": "Unable to load collections", "UnableToLoadCustomFormats": "Unable to load Custom Formats", "UnableToLoadDelayProfiles": "Unable to load Delay Profiles", "UnableToLoadDownloadClientOptions": "Unable to load download client options", @@ -1114,7 +1130,6 @@ "Wiki": "Wiki", "WouldYouLikeToRestoreBackup": "Would you like to restore the backup {0} ?", "Year": "Year", - "Yes": "Yes", "YesCancel": "Yes, Cancel", "YesMoveFiles": "Yes, Move the Files", "Yesterday": "Yesterday", diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/GrabbedReleaseQualitySpecification.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/GrabbedReleaseQualitySpecification.cs index eceb8693a..0592f3b3e 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/GrabbedReleaseQualitySpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/GrabbedReleaseQualitySpecification.cs @@ -4,7 +4,6 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.History; -using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; diff --git a/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs index 55c470b82..b2323f7b6 100644 --- a/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs +++ b/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Credits; namespace NzbDrone.Core.MetadataSource @@ -9,6 +10,7 @@ namespace NzbDrone.Core.MetadataSource { MovieMetadata GetMovieByImdbId(string imdbId); Tuple> GetMovieInfo(int tmdbId); + MovieCollection GetCollectionInfo(int tmdbId); List GetBulkMovieInfo(List tmdbIds); HashSet GetChangedMovies(DateTime startTime); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CollectionResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CollectionResource.cs index fb4194200..65603bcac 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CollectionResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CollectionResource.cs @@ -5,7 +5,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public class CollectionResource { public string Name { get; set; } + public string Overview { get; set; } public int TmdbId { get; set; } public List Images { get; set; } + public List Parts { get; set; } } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index c1adda9b7..29d21c814 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Movies; using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Credits; using NzbDrone.Core.Movies.Translations; using NzbDrone.Core.Parser; @@ -101,6 +102,35 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return new Tuple>(movie, credits.ToList()); } + public MovieCollection GetCollectionInfo(int tmdbId) + { + var httpRequest = _radarrMetadata.Create() + .SetSegment("route", "movie/collection") + .Resource(tmdbId.ToString()) + .Build(); + + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; + + var httpResponse = _httpClient.Get(httpRequest); + + if (httpResponse.HasHttpError) + { + if (httpResponse.StatusCode == HttpStatusCode.NotFound) + { + throw new MovieNotFoundException(tmdbId); + } + else + { + throw new HttpException(httpRequest, httpResponse); + } + } + + var collection = MapCollection(httpResponse.Resource); + + return collection; + } + public List GetBulkMovieInfo(List tmdbIds) { var httpRequest = _radarrMetadata.Create() @@ -257,7 +287,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook if (resource.Collection != null) { - movie.Collection = new MovieCollection { Name = resource.Collection.Name, TmdbId = resource.Collection.TmdbId }; + movie.CollectionTmdbId = resource.Collection.TmdbId; + movie.CollectionTitle = resource.Collection.Name; } return movie; @@ -470,6 +501,22 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return movie; } + private MovieCollection MapCollection(CollectionResource arg) + { + var collection = new MovieCollection + { + TmdbId = arg.TmdbId, + Title = arg.Name, + Overview = arg.Overview, + CleanTitle = arg.Name.CleanMovieTitle(), + SortTitle = Parser.Parser.NormalizeTitle(arg.Name), + Images = arg.Images?.Select(MapImage).ToList() ?? new List(), + Movies = arg.Parts?.Select(x => MapMovie(x)).ToList() ?? new List() + }; + + return collection; + } + private static Credit MapCast(CastResource arg) { var newActor = new Credit diff --git a/src/NzbDrone.Core/Movies/AddMovieService.cs b/src/NzbDrone.Core/Movies/AddMovieService.cs index b2a123532..71b94ea88 100644 --- a/src/NzbDrone.Core/Movies/AddMovieService.cs +++ b/src/NzbDrone.Core/Movies/AddMovieService.cs @@ -8,6 +8,7 @@ using NLog; using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser; using NzbDrone.Core.RootFolders; @@ -76,6 +77,7 @@ namespace NzbDrone.Core.Movies movie = SetPropertiesAndValidate(movie); movie.Added = added; + moviesToAdd.Add(movie); } catch (ValidationException ex) diff --git a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleService.cs b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleService.cs index e0fff47f5..58ae90d45 100644 --- a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleService.cs +++ b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleService.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Movies.Events; diff --git a/src/NzbDrone.Core/Movies/Collections/AddMovieCollectionService.cs b/src/NzbDrone.Core/Movies/Collections/AddMovieCollectionService.cs new file mode 100644 index 000000000..4b4788bb8 --- /dev/null +++ b/src/NzbDrone.Core/Movies/Collections/AddMovieCollectionService.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Movies.Collections +{ + public interface IAddMovieCollectionService + { + MovieCollection AddMovieCollection(MovieCollection newCollection); + } + + public class AddMovieCollectionService : IAddMovieCollectionService + { + private readonly IMovieCollectionService _collectionService; + private readonly IProvideMovieInfo _movieInfo; + private readonly Logger _logger; + + public AddMovieCollectionService(IMovieCollectionService collectionService, + IProvideMovieInfo movieInfo, + Logger logger) + { + _collectionService = collectionService; + _movieInfo = movieInfo; + _logger = logger; + } + + public MovieCollection AddMovieCollection(MovieCollection newCollection) + { + Ensure.That(newCollection, () => newCollection).IsNotNull(); + + var existingCollection = _collectionService.FindByTmdbId(newCollection.TmdbId); + + if (existingCollection != null) + { + return existingCollection; + } + + newCollection = AddSkyhookData(newCollection); + newCollection = SetPropertiesAndValidate(newCollection); + + _logger.Info("Adding Collection {0}", newCollection); + + _collectionService.AddCollection(newCollection); + + return newCollection; + } + + private MovieCollection AddSkyhookData(MovieCollection newCollection) + { + MovieCollection collection; + + try + { + collection = _movieInfo.GetCollectionInfo(newCollection.TmdbId); + } + catch (MovieNotFoundException) + { + _logger.Error("TmdbId {0} was not found, it may have been removed from TMDb.", newCollection.TmdbId); + + throw new ValidationException(new List + { + new ValidationFailure("TmdbId", $"A collection with this ID was not found.", newCollection.TmdbId) + }); + } + + collection.ApplyChanges(newCollection); + + return collection; + } + + private MovieCollection SetPropertiesAndValidate(MovieCollection newCollection) + { + newCollection.CleanTitle = newCollection.Title.CleanMovieTitle(); + newCollection.SortTitle = MovieTitleNormalizer.Normalize(newCollection.Title, newCollection.TmdbId); + newCollection.Added = DateTime.UtcNow; + + return newCollection; + } + } +} diff --git a/src/NzbDrone.Core/Movies/Collections/MovieCollection.cs b/src/NzbDrone.Core/Movies/Collections/MovieCollection.cs new file mode 100644 index 000000000..18a33b28f --- /dev/null +++ b/src/NzbDrone.Core/Movies/Collections/MovieCollection.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Movies.Collections +{ + public class MovieCollection : ModelBase + { + public MovieCollection() + { + Images = new List(); + } + + public string Title { get; set; } + public string CleanTitle { get; set; } + public string SortTitle { get; set; } + public int TmdbId { get; set; } + public string Overview { get; set; } + public bool Monitored { get; set; } + public int QualityProfileId { get; set; } + public string RootFolderPath { get; set; } + public bool SearchOnAdd { get; set; } + public MovieStatusType MinimumAvailability { get; set; } + public DateTime? LastInfoSync { get; set; } + public List Images { get; set; } + public DateTime Added { get; set; } + public List Movies { get; set; } + + public void ApplyChanges(MovieCollection otherCollection) + { + TmdbId = otherCollection.TmdbId; + + Monitored = otherCollection.Monitored; + SearchOnAdd = otherCollection.SearchOnAdd; + QualityProfileId = otherCollection.QualityProfileId; + MinimumAvailability = otherCollection.MinimumAvailability; + RootFolderPath = otherCollection.RootFolderPath; + } + } +} diff --git a/src/NzbDrone.Core/Movies/Collections/MovieCollectionAddedHandler.cs b/src/NzbDrone.Core/Movies/Collections/MovieCollectionAddedHandler.cs new file mode 100644 index 000000000..bf448560d --- /dev/null +++ b/src/NzbDrone.Core/Movies/Collections/MovieCollectionAddedHandler.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Movies.Commands; +using NzbDrone.Core.Movies.Events; + +namespace NzbDrone.Core.Movies +{ + public class MovieCollectionAddedHandler : IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + + public MovieCollectionAddedHandler(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(CollectionAddedEvent message) + { + _commandQueueManager.Push(new RefreshCollectionsCommand(new List { message.Collection.Id })); + } + } +} diff --git a/src/NzbDrone.Core/Movies/Collections/MovieCollectionRepository.cs b/src/NzbDrone.Core/Movies/Collections/MovieCollectionRepository.cs new file mode 100644 index 000000000..2017147fc --- /dev/null +++ b/src/NzbDrone.Core/Movies/Collections/MovieCollectionRepository.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Movies.Collections +{ + public interface IMovieCollectionRepository : IBasicRepository + { + public MovieCollection GetByTmdbId(int tmdbId); + bool UpsertMany(List data); + } + + public class MovieCollectionRepository : BasicRepository, IMovieCollectionRepository + { + public MovieCollectionRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public MovieCollection GetByTmdbId(int tmdbId) + { + return Query(x => x.TmdbId == tmdbId).FirstOrDefault(); + } + + public List GetByTmdbId(List tmdbIds) + { + return Query(x => Enumerable.Contains(tmdbIds, x.TmdbId)); + } + + public bool UpsertMany(List data) + { + var existingMetadata = GetByTmdbId(data.Select(x => x.TmdbId).ToList()); + var updateCollectionList = new List(); + var addCollectionList = new List(); + int upToDateMetadataCount = 0; + + foreach (var collection in data) + { + var existing = existingMetadata.SingleOrDefault(x => x.TmdbId == collection.TmdbId); + if (existing != null) + { + // populate Id in remote data + collection.Id = existing.Id; + + // responses vary, so try adding remote to what we have + if (!collection.Equals(existing)) + { + updateCollectionList.Add(collection); + } + else + { + upToDateMetadataCount++; + } + } + else + { + addCollectionList.Add(collection); + } + } + + UpdateMany(updateCollectionList); + InsertMany(addCollectionList); + + return updateCollectionList.Count > 0 || addCollectionList.Count > 0; + } + } +} diff --git a/src/NzbDrone.Core/Movies/Collections/MovieCollectionService.cs b/src/NzbDrone.Core/Movies/Collections/MovieCollectionService.cs new file mode 100644 index 000000000..efcaab428 --- /dev/null +++ b/src/NzbDrone.Core/Movies/Collections/MovieCollectionService.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Movies.Events; + +namespace NzbDrone.Core.Movies.Collections +{ + public interface IMovieCollectionService + { + MovieCollection AddCollection(MovieCollection collection); + MovieCollection GetCollection(int id); + MovieCollection FindByTmdbId(int tmdbId); + IEnumerable GetCollections(IEnumerable ids); + List GetAllCollections(); + MovieCollection UpdateCollection(MovieCollection collection); + void RemoveCollection(MovieCollection collection); + bool Upsert(MovieCollection collection); + bool UpsertMany(List collections); + } + + public class MovieCollectionService : IMovieCollectionService, IHandleAsync + { + private readonly IMovieCollectionRepository _repo; + private readonly IMovieMetadataService _movieMetadataService; + private readonly IEventAggregator _eventAggregator; + + public MovieCollectionService(IMovieCollectionRepository repo, IMovieMetadataService movieMetadataService, IEventAggregator eventAggregator) + { + _repo = repo; + _movieMetadataService = movieMetadataService; + _eventAggregator = eventAggregator; + } + + public MovieCollection AddCollection(MovieCollection newCollection) + { + var existing = _repo.GetByTmdbId(newCollection.TmdbId); + + if (existing == null) + { + var collection = _repo.Insert(newCollection); + + _eventAggregator.PublishEvent(new CollectionAddedEvent(collection)); + + return collection; + } + + return existing; + } + + public MovieCollection GetCollection(int id) + { + return _repo.Get(id); + } + + public IEnumerable GetCollections(IEnumerable ids) + { + return _repo.Get(ids); + } + + public List GetAllCollections() + { + return _repo.All().ToList(); + } + + public MovieCollection UpdateCollection(MovieCollection collection) + { + var storedCollection = GetCollection(collection.Id); + + var updatedCollection = _repo.Update(collection); + + _eventAggregator.PublishEvent(new CollectionEditedEvent(updatedCollection, storedCollection)); + + return updatedCollection; + } + + public void RemoveCollection(MovieCollection collection) + { + _repo.Delete(collection); + + _eventAggregator.PublishEvent(new CollectionDeletedEvent(collection)); + } + + public bool Upsert(MovieCollection collection) + { + return _repo.UpsertMany(new List { collection }); + } + + public bool UpsertMany(List collections) + { + return _repo.UpsertMany(collections); + } + + public void HandleAsync(MoviesDeletedEvent message) + { + var collections = message.Movies.Select(x => x.MovieMetadata.Value.CollectionTmdbId).Distinct(); + + foreach (var collectionTmdbId in collections) + { + if (collectionTmdbId == 0 || _movieMetadataService.GetMoviesByCollectionTmdbId(collectionTmdbId).Any()) + { + continue; + } + + var collection = FindByTmdbId(collectionTmdbId); + + _eventAggregator.PublishEvent(new CollectionDeletedEvent(collection)); + + _repo.Delete(collectionTmdbId); + } + } + + public MovieCollection FindByTmdbId(int tmdbId) + { + return _repo.GetByTmdbId(tmdbId); + } + } +} diff --git a/src/NzbDrone.Core/Movies/Commands/RefreshCollectionsCommand.cs b/src/NzbDrone.Core/Movies/Commands/RefreshCollectionsCommand.cs new file mode 100644 index 000000000..e33e40277 --- /dev/null +++ b/src/NzbDrone.Core/Movies/Commands/RefreshCollectionsCommand.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Movies.Commands +{ + public class RefreshCollectionsCommand : Command + { + public List CollectionIds { get; set; } + + public RefreshCollectionsCommand() + { + CollectionIds = new List(); + } + + public RefreshCollectionsCommand(List collectionIds) + { + CollectionIds = collectionIds; + } + + public override bool SendUpdatesToClient => true; + + public override bool UpdateScheduledTask => !CollectionIds.Any(); + } +} diff --git a/src/NzbDrone.Core/Movies/Credits/CreditService.cs b/src/NzbDrone.Core/Movies/Credits/CreditService.cs index 724b560a5..cf6743612 100644 --- a/src/NzbDrone.Core/Movies/Credits/CreditService.cs +++ b/src/NzbDrone.Core/Movies/Credits/CreditService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Movies.Events; diff --git a/src/NzbDrone.Core/Movies/Events/CollectionAddedEvent.cs b/src/NzbDrone.Core/Movies/Events/CollectionAddedEvent.cs new file mode 100644 index 000000000..19e1351ff --- /dev/null +++ b/src/NzbDrone.Core/Movies/Events/CollectionAddedEvent.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Movies.Collections; + +namespace NzbDrone.Core.Movies.Events +{ + public class CollectionAddedEvent : IEvent + { + public MovieCollection Collection { get; private set; } + + public CollectionAddedEvent(MovieCollection collection) + { + Collection = collection; + } + } +} diff --git a/src/NzbDrone.Core/Movies/Events/CollectionDeletedEvent.cs b/src/NzbDrone.Core/Movies/Events/CollectionDeletedEvent.cs new file mode 100644 index 000000000..c03442e61 --- /dev/null +++ b/src/NzbDrone.Core/Movies/Events/CollectionDeletedEvent.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Movies.Collections; + +namespace NzbDrone.Core.Movies.Events +{ + public class CollectionDeletedEvent : IEvent + { + public MovieCollection Collection { get; private set; } + + public CollectionDeletedEvent(MovieCollection collection) + { + Collection = collection; + } + } +} diff --git a/src/NzbDrone.Core/Movies/Events/CollectionEditedEvent.cs b/src/NzbDrone.Core/Movies/Events/CollectionEditedEvent.cs new file mode 100644 index 000000000..b5453068f --- /dev/null +++ b/src/NzbDrone.Core/Movies/Events/CollectionEditedEvent.cs @@ -0,0 +1,17 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Movies.Collections; + +namespace NzbDrone.Core.Movies.Events +{ + public class CollectionEditedEvent : IEvent + { + public MovieCollection Collection { get; private set; } + public MovieCollection OldCollection { get; private set; } + + public CollectionEditedEvent(MovieCollection collection, MovieCollection oldCollection) + { + Collection = collection; + OldCollection = oldCollection; + } + } +} diff --git a/src/NzbDrone.Core/Movies/MonitoringOptions.cs b/src/NzbDrone.Core/Movies/MonitoringOptions.cs index e6145184d..87519bbb1 100644 --- a/src/NzbDrone.Core/Movies/MonitoringOptions.cs +++ b/src/NzbDrone.Core/Movies/MonitoringOptions.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Movies { @@ -6,5 +6,13 @@ namespace NzbDrone.Core.Movies { public bool IgnoreEpisodesWithFiles { get; set; } public bool IgnoreEpisodesWithoutFiles { get; set; } + public MonitorTypes Monitor { get; set; } + } + + public enum MonitorTypes + { + MovieOnly, + MovieAndCollection, + None } } diff --git a/src/NzbDrone.Core/Movies/MovieCollection.cs b/src/NzbDrone.Core/Movies/MovieCollection.cs deleted file mode 100644 index 67500bd25..000000000 --- a/src/NzbDrone.Core/Movies/MovieCollection.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Movies -{ - public class MovieCollection : IEmbeddedDocument - { - public MovieCollection() - { - Images = new List(); - } - - public string Name { get; set; } - public int TmdbId { get; set; } - public List Images { get; set; } - } -} diff --git a/src/NzbDrone.Core/Movies/MovieMetadata.cs b/src/NzbDrone.Core/Movies/MovieMetadata.cs index b1082b198..193d3b5fa 100644 --- a/src/NzbDrone.Core/Movies/MovieMetadata.cs +++ b/src/NzbDrone.Core/Movies/MovieMetadata.cs @@ -31,7 +31,8 @@ namespace NzbDrone.Core.Movies public int Year { get; set; } public Ratings Ratings { get; set; } - public MovieCollection Collection { get; set; } + public int CollectionTmdbId { get; set; } + public string CollectionTitle { get; set; } public DateTime? LastInfoSync { get; set; } public int Runtime { get; set; } public string Website { get; set; } diff --git a/src/NzbDrone.Core/Movies/MovieMetadataRepository.cs b/src/NzbDrone.Core/Movies/MovieMetadataRepository.cs index 78f9b7a29..180db057e 100644 --- a/src/NzbDrone.Core/Movies/MovieMetadataRepository.cs +++ b/src/NzbDrone.Core/Movies/MovieMetadataRepository.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Movies { MovieMetadata FindByTmdbId(int tmdbId); List FindById(List tmdbIds); + List GetMoviesByCollectionTmdbId(int collectionId); bool UpsertMany(List data); } @@ -33,6 +34,11 @@ namespace NzbDrone.Core.Movies return Query(x => Enumerable.Contains(tmdbIds, x.TmdbId)); } + public List GetMoviesByCollectionTmdbId(int collectionId) + { + return Query(x => x.CollectionTmdbId == collectionId); + } + public bool UpsertMany(List data) { var existingMetadata = FindById(data.Select(x => x.TmdbId).ToList()); diff --git a/src/NzbDrone.Core/Movies/MovieMetadataService.cs b/src/NzbDrone.Core/Movies/MovieMetadataService.cs index 172296e32..30a0a9d23 100644 --- a/src/NzbDrone.Core/Movies/MovieMetadataService.cs +++ b/src/NzbDrone.Core/Movies/MovieMetadataService.cs @@ -6,6 +6,7 @@ namespace NzbDrone.Core.Movies { MovieMetadata Get(int id); MovieMetadata FindByTmdbId(int tmdbid); + List GetMoviesByCollectionTmdbId(int collectionId); bool Upsert(MovieMetadata movie); bool UpsertMany(List movies); } @@ -24,6 +25,11 @@ namespace NzbDrone.Core.Movies return _movieMetadataRepository.FindByTmdbId(tmdbid); } + public List GetMoviesByCollectionTmdbId(int collectionId) + { + return _movieMetadataRepository.GetMoviesByCollectionTmdbId(collectionId); + } + public MovieMetadata Get(int id) { return _movieMetadataRepository.Get(id); diff --git a/src/NzbDrone.Core/Movies/MovieRepository.cs b/src/NzbDrone.Core/Movies/MovieRepository.cs index 13a983bc3..5d3acbacd 100644 --- a/src/NzbDrone.Core/Movies/MovieRepository.cs +++ b/src/NzbDrone.Core/Movies/MovieRepository.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Dapper; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; @@ -23,6 +22,7 @@ namespace NzbDrone.Core.Movies List MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); PagingSpec MoviesWithoutFiles(PagingSpec pagingSpec); List GetMoviesByFileId(int fileId); + List GetMoviesByCollectionTmdbId(int collectionId); void SetFileId(int fileId, int movieId); PagingSpec MoviesWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff); Movie FindByPath(string path); @@ -221,6 +221,11 @@ namespace NzbDrone.Core.Movies return Query(x => x.MovieFileId == fileId); } + public List GetMoviesByCollectionTmdbId(int collectionId) + { + return Query(x => x.MovieMetadata.Value.CollectionTmdbId == collectionId); + } + public void SetFileId(int fileId, int movieId) { SetFields(new Movie { Id = movieId, MovieFileId = fileId }, movie => movie.MovieFileId); diff --git a/src/NzbDrone.Core/Movies/MovieScannedHandler.cs b/src/NzbDrone.Core/Movies/MovieScannedHandler.cs index 41a3a0885..63fe3c7f6 100644 --- a/src/NzbDrone.Core/Movies/MovieScannedHandler.cs +++ b/src/NzbDrone.Core/Movies/MovieScannedHandler.cs @@ -4,6 +4,7 @@ using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Movies.Collections; namespace NzbDrone.Core.Movies { @@ -11,15 +12,18 @@ namespace NzbDrone.Core.Movies IHandle { private readonly IMovieService _movieService; + private readonly IMovieCollectionService _collectionService; private readonly IManageCommandQueue _commandQueueManager; private readonly Logger _logger; public MovieScannedHandler(IMovieService movieService, + IMovieCollectionService collectionService, IManageCommandQueue commandQueueManager, Logger logger) { _movieService = movieService; + _collectionService = collectionService; _commandQueueManager = commandQueueManager; _logger = logger; } @@ -38,6 +42,14 @@ namespace NzbDrone.Core.Movies _commandQueueManager.Push(new MoviesSearchCommand { MovieIds = new List { movie.Id } }); } + if (movie.AddOptions.Monitor == MonitorTypes.MovieAndCollection && movie.MovieMetadata.Value.CollectionTmdbId > 0) + { + var collection = _collectionService.FindByTmdbId(movie.MovieMetadata.Value.CollectionTmdbId); + collection.Monitored = true; + + _collectionService.UpdateCollection(collection); + } + movie.AddOptions = null; _movieService.RemoveAddOptions(movie); } diff --git a/src/NzbDrone.Core/Movies/MovieService.cs b/src/NzbDrone.Core/Movies/MovieService.cs index 4373cb364..ef01187d9 100644 --- a/src/NzbDrone.Core/Movies/MovieService.cs +++ b/src/NzbDrone.Core/Movies/MovieService.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Movies List AllMovieTmdbIds(); bool MovieExists(Movie movie); List GetMoviesByFileId(int fileId); + List GetMoviesByCollectionTmdbId(int collectionId); List GetMoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); PagingSpec MoviesWithoutFiles(PagingSpec pagingSpec); void SetFileId(Movie movie, MovieFile movieFile); @@ -88,15 +89,17 @@ namespace NzbDrone.Core.Movies public Movie AddMovie(Movie newMovie) { - _movieRepository.Insert(newMovie); - _eventAggregator.PublishEvent(new MovieAddedEvent(GetMovie(newMovie.Id))); + var movie = _movieRepository.Insert(newMovie); - return newMovie; + _eventAggregator.PublishEvent(new MovieAddedEvent(GetMovie(movie.Id))); + + return movie; } public List AddMovies(List newMovies) { _movieRepository.InsertMany(newMovies); + _eventAggregator.PublishEvent(new MoviesImportedEvent(newMovies.Select(s => s.Id).ToList())); return newMovies; @@ -293,6 +296,11 @@ namespace NzbDrone.Core.Movies return _movieRepository.GetMoviesByFileId(fileId); } + public List GetMoviesByCollectionTmdbId(int collectionId) + { + return _movieRepository.GetMoviesByCollectionTmdbId(collectionId); + } + public List GetMoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) { var movies = _movieRepository.MoviesBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored); diff --git a/src/NzbDrone.Core/Movies/RefreshCollectionService.cs b/src/NzbDrone.Core/Movies/RefreshCollectionService.cs new file mode 100644 index 000000000..671471b4e --- /dev/null +++ b/src/NzbDrone.Core/Movies/RefreshCollectionService.cs @@ -0,0 +1,159 @@ +using System; +using System.Linq; +using NLog; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Movies.Collections; +using NzbDrone.Core.Movies.Commands; +using NzbDrone.Core.Movies.Events; + +namespace NzbDrone.Core.Movies +{ + public class RefreshCollectionService : IExecute, IHandle + { + private readonly IProvideMovieInfo _movieInfo; + private readonly IMovieCollectionService _collectionService; + private readonly IMovieService _movieService; + private readonly IMovieMetadataService _movieMetadataService; + private readonly IAddMovieService _addMovieService; + + private readonly Logger _logger; + + public RefreshCollectionService(IProvideMovieInfo movieInfo, + IMovieCollectionService collectionService, + IMovieService movieService, + IMovieMetadataService movieMetadataService, + IAddMovieService addMovieService, + Logger logger) + { + _movieInfo = movieInfo; + _collectionService = collectionService; + _movieService = movieService; + _movieMetadataService = movieMetadataService; + _addMovieService = addMovieService; + _logger = logger; + } + + private MovieCollection RefreshCollectionInfo(int collectionId) + { + // Get the movie before updating, that way any changes made to the movie after the refresh started, + // but before this movie was refreshed won't be lost. + var collection = _collectionService.GetCollection(collectionId); + + _logger.ProgressInfo("Updating info for {0}", collection.Title); + + MovieCollection collectionInfo; + + try + { + collectionInfo = _movieInfo.GetCollectionInfo(collection.TmdbId); + } + catch (MovieNotFoundException) + { + _collectionService.RemoveCollection(collection); + _logger.Debug("Removing collection not present on TMDb for {0}", collection.Title); + + throw; + } + + collection.Title = collectionInfo.Title; + collection.Overview = collectionInfo.Overview; + collection.CleanTitle = collectionInfo.CleanTitle; + collection.SortTitle = collectionInfo.SortTitle; + collection.LastInfoSync = DateTime.UtcNow; + collection.Images = collectionInfo.Images; + + collectionInfo.Movies.ForEach(x => x.CollectionTmdbId = collection.TmdbId); + _movieMetadataService.UpsertMany(collectionInfo.Movies); + + _logger.Debug("Finished collection refresh for {0}", collection.Title); + + _collectionService.UpdateCollection(collection); + + return collection; + } + + public bool ShouldRefresh(MovieCollection collection) + { + if (collection.LastInfoSync == null || collection.LastInfoSync < DateTime.UtcNow.AddDays(-15)) + { + _logger.Trace("Collection {0} last updated more than 15 days ago, should refresh.", collection.Title); + return true; + } + + if (collection.LastInfoSync >= DateTime.UtcNow.AddHours(-6)) + { + _logger.Trace("Collection {0} last updated less than 6 hours ago, should not be refreshed.", collection.Title); + return false; + } + + return false; + } + + private void SyncCollectionMovies(MovieCollection collection) + { + if (collection.Monitored) + { + var existingMovies = _movieService.AllMovieTmdbIds(); + var collectionMovies = _movieMetadataService.GetMoviesByCollectionTmdbId(collection.TmdbId); + + _addMovieService.AddMovies(collectionMovies.Where(m => !existingMovies.Contains(m.TmdbId)).Select(m => new Movie + { + TmdbId = m.TmdbId, + Title = m.Title, + ProfileId = collection.QualityProfileId, + RootFolderPath = collection.RootFolderPath, + MinimumAvailability = collection.MinimumAvailability, + AddOptions = new AddMovieOptions + { + SearchForMovie = collection.SearchOnAdd + }, + Monitored = true + }).ToList()); + } + } + + public void Execute(RefreshCollectionsCommand message) + { + if (message.CollectionIds.Any()) + { + foreach (var collectionId in message.CollectionIds) + { + var newCollection = RefreshCollectionInfo(collectionId); + SyncCollectionMovies(newCollection); + } + } + else + { + var allCollections = _collectionService.GetAllCollections().OrderBy(c => c.SortTitle).ToList(); + + foreach (var collection in allCollections) + { + try + { + var newCollection = collection; + + if (ShouldRefresh(collection) || message.Trigger == CommandTrigger.Manual) + { + newCollection = RefreshCollectionInfo(collection.Id); + } + + SyncCollectionMovies(newCollection); + } + catch (MovieNotFoundException) + { + _logger.Error("Collection '{0}' (TMDb {1}) was not found, it may have been removed from The Movie Database.", collection.Title, collection.TmdbId); + } + } + } + } + + public void Handle(CollectionEditedEvent message) + { + SyncCollectionMovies(message.Collection); + } + } +} diff --git a/src/NzbDrone.Core/Movies/RefreshMovieService.cs b/src/NzbDrone.Core/Movies/RefreshMovieService.cs index 40a08bafe..6804cd69a 100644 --- a/src/NzbDrone.Core/Movies/RefreshMovieService.cs +++ b/src/NzbDrone.Core/Movies/RefreshMovieService.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; @@ -12,10 +10,12 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Commands; using NzbDrone.Core.Movies.Credits; using NzbDrone.Core.Movies.Events; using NzbDrone.Core.Movies.Translations; +using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.Movies { @@ -23,7 +23,9 @@ namespace NzbDrone.Core.Movies { private readonly IProvideMovieInfo _movieInfo; private readonly IMovieService _movieService; + private readonly IAddMovieCollectionService _movieCollectionService; private readonly IMovieMetadataService _movieMetadataService; + private readonly IRootFolderService _folderService; private readonly IMovieTranslationService _movieTranslationService; private readonly IAlternativeTitleService _titleService; private readonly ICreditService _creditService; @@ -36,7 +38,9 @@ namespace NzbDrone.Core.Movies public RefreshMovieService(IProvideMovieInfo movieInfo, IMovieService movieService, + IAddMovieCollectionService movieCollectionService, IMovieMetadataService movieMetadataService, + IRootFolderService folderService, IMovieTranslationService movieTranslationService, IAlternativeTitleService titleService, ICreditService creditService, @@ -48,7 +52,9 @@ namespace NzbDrone.Core.Movies { _movieInfo = movieInfo; _movieService = movieService; + _movieCollectionService = movieCollectionService; _movieMetadataService = movieMetadataService; + _folderService = folderService; _movieTranslationService = movieTranslationService; _titleService = titleService; _creditService = creditService; @@ -105,7 +111,6 @@ namespace NzbDrone.Core.Movies movieMetadata.LastInfoSync = DateTime.UtcNow; movieMetadata.Runtime = movieInfo.Runtime; movieMetadata.Ratings = movieInfo.Ratings; - movieMetadata.Collection = movieInfo.Collection; //movie.Genres = movieInfo.Genres; movieMetadata.Certification = movieInfo.Certification; @@ -124,6 +129,24 @@ namespace NzbDrone.Core.Movies movieMetadata.Recommendations = movieInfo.Recommendations; movieMetadata.Popularity = movieInfo.Popularity; + // add collection + if (movieInfo.CollectionTmdbId > 0) + { + var newCollection = _movieCollectionService.AddMovieCollection(new MovieCollection + { + TmdbId = movieInfo.CollectionTmdbId, + Title = movieInfo.CollectionTitle, + Monitored = movie.AddOptions?.Monitor == MonitorTypes.MovieAndCollection, + SearchOnAdd = movie.AddOptions?.SearchForMovie ?? false, + QualityProfileId = movie.ProfileId, + MinimumAvailability = movie.MinimumAvailability, + RootFolderPath = _folderService.GetBestRootFolderPath(movie.Path) + }); + + movieMetadata.CollectionTmdbId = newCollection.TmdbId; + movieMetadata.CollectionTitle = newCollection.Title; + } + movieMetadata.AlternativeTitles = _titleService.UpdateTitles(movieInfo.AlternativeTitles, movieMetadata); _movieTranslationService.UpdateTranslations(movieInfo.Translations, movieMetadata); diff --git a/src/NzbDrone.Core/Movies/Translations/MovieTranslationService.cs b/src/NzbDrone.Core/Movies/Translations/MovieTranslationService.cs index c50750835..f531ea55d 100644 --- a/src/NzbDrone.Core/Movies/Translations/MovieTranslationService.cs +++ b/src/NzbDrone.Core/Movies/Translations/MovieTranslationService.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Languages; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Movies.Events; diff --git a/src/NzbDrone.Core/Notifications/DeleteMessage.cs b/src/NzbDrone.Core/Notifications/DeleteMessage.cs index ec700f7b4..b2e2ca669 100644 --- a/src/NzbDrone.Core/Notifications/DeleteMessage.cs +++ b/src/NzbDrone.Core/Notifications/DeleteMessage.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies; diff --git a/src/NzbDrone.Core/Notifications/Email/Email.cs b/src/NzbDrone.Core/Notifications/Email/Email.cs index 71b787447..07a232120 100644 --- a/src/NzbDrone.Core/Notifications/Email/Email.cs +++ b/src/NzbDrone.Core/Notifications/Email/Email.cs @@ -8,7 +8,6 @@ using MimeKit; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Dispatchers; -using NzbDrone.Core.Security; namespace NzbDrone.Core.Notifications.Email { diff --git a/src/NzbDrone.Core/Notifications/MovieDeleteMessage.cs b/src/NzbDrone.Core/Notifications/MovieDeleteMessage.cs index 8c89d0165..de3294057 100644 --- a/src/NzbDrone.Core/Notifications/MovieDeleteMessage.cs +++ b/src/NzbDrone.Core/Notifications/MovieDeleteMessage.cs @@ -1,4 +1,3 @@ -using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies; namespace NzbDrone.Core.Notifications diff --git a/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs b/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs index 4b9397749..9afa37927 100644 --- a/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs +++ b/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Specialized; -using System.Net; using FluentValidation.Results; using NLog; using NzbDrone.Common.Http; diff --git a/src/NzbDrone.Core/Notifications/NotificationBase.cs b/src/NzbDrone.Core/Notifications/NotificationBase.cs index 9b73e1a43..e99d4e1ed 100644 --- a/src/NzbDrone.Core/Notifications/NotificationBase.cs +++ b/src/NzbDrone.Core/Notifications/NotificationBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using FluentValidation.Results; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies; diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs index c121a8734..51321cf07 100644 --- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs +++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; diff --git a/src/NzbDrone.Core/Notifications/Simplepush/SimplepushProxy.cs b/src/NzbDrone.Core/Notifications/Simplepush/SimplepushProxy.cs index 5254b73de..f0f6788f2 100644 --- a/src/NzbDrone.Core/Notifications/Simplepush/SimplepushProxy.cs +++ b/src/NzbDrone.Core/Notifications/Simplepush/SimplepushProxy.cs @@ -1,7 +1,6 @@ using System; using FluentValidation.Results; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; namespace NzbDrone.Core.Notifications.Simplepush diff --git a/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs b/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs index 92daff3b4..af8b211ef 100644 --- a/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs +++ b/src/NzbDrone.Core/Notifications/Simplepush/SimplepushSettings.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index 97f59b351..d3f1e52a9 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookApplicationUpdatePayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookApplicationUpdatePayload.cs index e05be69bc..66a6ff382 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookApplicationUpdatePayload.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookApplicationUpdatePayload.cs @@ -1,5 +1,3 @@ -using NzbDrone.Core.HealthCheck; - namespace NzbDrone.Core.Notifications.Webhook { public class WebhookApplicationUpdatePayload : WebhookPayload diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieDeletePayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieDeletePayload.cs index 8540110f5..4b111ef26 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieDeletePayload.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieDeletePayload.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace NzbDrone.Core.Notifications.Webhook { public class WebhookMovieDeletePayload : WebhookPayload diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieFileDeletePayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieFileDeletePayload.cs index 851fa285c..c3c85edbd 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieFileDeletePayload.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookMovieFileDeletePayload.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Notifications.Webhook diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index ababf2787..c62ef3ebe 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.MediaFiles; @@ -268,7 +267,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Movie CleanOriginalTitle}"] = m => CleanTitle(movie.MovieMetadata.Value.OriginalTitle) ?? string.Empty; tokenHandlers["{Movie Certification}"] = m => movie.MovieMetadata.Value.Certification ?? string.Empty; - tokenHandlers["{Movie Collection}"] = m => movie.MovieMetadata.Value.Collection?.Name ?? string.Empty; + tokenHandlers["{Movie Collection}"] = m => movie.MovieMetadata.Value.CollectionTitle ?? string.Empty; } private string GetLanguageTitle(Movie movie, string isoCodes) diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 70e4aeecb..ff887adf2 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -54,7 +54,8 @@ namespace NzbDrone.Core.Organizer { Title = "The Movie: Title", OriginalTitle = "The Original Movie Title", - Collection = new MovieCollection { Name = "The Movie Collection", TmdbId = 123654 }, + CollectionTitle = "The Movie Collection", + CollectionTmdbId = 123654, Certification = "R", Year = 2010, ImdbId = "tt0066921", diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs index 1469a5a60..5bd683176 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguages.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices.ComTypes; using NzbDrone.Core.Languages; using NzbDrone.Core.Organizer; diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 69e8b9f20..991531ede 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Languages; diff --git a/src/NzbDrone.Core/Profiles/ProfileService.cs b/src/NzbDrone.Core/Profiles/ProfileService.cs index a21478edd..09e79ed4a 100644 --- a/src/NzbDrone.Core/Profiles/ProfileService.cs +++ b/src/NzbDrone.Core/Profiles/ProfileService.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Languages; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Profiles @@ -33,18 +34,21 @@ namespace NzbDrone.Core.Profiles private readonly ICustomFormatService _formatService; private readonly IMovieService _movieService; private readonly IImportListFactory _importListFactory; + private readonly IMovieCollectionService _collectionService; private readonly Logger _logger; public ProfileService(IProfileRepository profileRepository, ICustomFormatService formatService, IMovieService movieService, IImportListFactory importListFactory, + IMovieCollectionService collectionService, Logger logger) { _profileRepository = profileRepository; _formatService = formatService; _movieService = movieService; _importListFactory = importListFactory; + _collectionService = collectionService; _logger = logger; } @@ -60,7 +64,7 @@ namespace NzbDrone.Core.Profiles public void Delete(int id) { - if (_movieService.GetAllMovies().Any(c => c.ProfileId == id) || _importListFactory.All().Any(c => c.ProfileId == id)) + if (_movieService.GetAllMovies().Any(c => c.ProfileId == id) || _importListFactory.All().Any(c => c.ProfileId == id) || _collectionService.GetAllCollections().Any(c => c.QualityProfileId == id)) { throw new ProfileInUseException(id); } diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index dd51a9d7a..c1a93c019 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -4,7 +4,6 @@ using System.Linq; using FluentValidation.Results; using Microsoft.Extensions.DependencyInjection; using NLog; -using NzbDrone.Common.Composition; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider.Events; diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs index 40bd68963..fe5e259b5 100644 --- a/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; diff --git a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs index 42b338f5f..4796a68e2 100644 --- a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs +++ b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; using NzbDrone.Core.Update.History; diff --git a/src/Radarr.Api.V3/Collections/CollectionController.cs b/src/Radarr.Api.V3/Collections/CollectionController.cs new file mode 100644 index 000000000..41f9823db --- /dev/null +++ b/src/Radarr.Api.V3/Collections/CollectionController.cs @@ -0,0 +1,128 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; +using NzbDrone.Core.Movies.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.SignalR; +using Radarr.Http; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V3.Collections +{ + [V3ApiController] + public class CollectionController : RestControllerWithSignalR, + IHandle, + IHandle, + IHandle + { + private readonly IMovieCollectionService _collectionService; + private readonly IMovieService _movieService; + private readonly IMovieMetadataService _movieMetadataService; + private readonly IBuildFileNames _fileNameBuilder; + + public CollectionController(IBroadcastSignalRMessage signalRBroadcaster, + IMovieCollectionService collectionService, + IMovieService movieService, + IMovieMetadataService movieMetadataService, + IBuildFileNames fileNameBuilder) + : base(signalRBroadcaster) + { + _collectionService = collectionService; + _movieService = movieService; + _movieMetadataService = movieMetadataService; + _fileNameBuilder = fileNameBuilder; + } + + protected override CollectionResource GetResourceById(int id) + { + return MapToResource(_collectionService.GetCollection(id)); + } + + [HttpGet] + public List GetCollections() + { + return _collectionService.GetAllCollections().Select(c => MapToResource(c)).ToList(); + } + + [RestPutById] + public ActionResult UpdateCollection(CollectionResource collectionResource) + { + var collection = _collectionService.GetCollection(collectionResource.Id); + + var model = collectionResource.ToModel(collection); + + var updatedMovie = _collectionService.UpdateCollection(model); + + return Accepted(updatedMovie.Id); + } + + [HttpPut] + public ActionResult UpdateCollections(CollectionUpdateResource collectionResources) + { + var collectionsToUpdate = _collectionService.GetCollections(collectionResources.Collections.Select(c => c.Id)); + var update = new List(); + + foreach (var c in collectionResources.Collections) + { + var collection = collectionsToUpdate.Single(n => n.Id == c.Id); + + if (c.Monitored.HasValue) + { + collection.Monitored = c.Monitored.Value; + } + + if (collectionResources.MonitorMovies.HasValue) + { + var movies = _movieService.GetMoviesByCollectionTmdbId(collection.TmdbId); + + movies.ForEach(c => c.Monitored = collectionResources.MonitorMovies.Value); + + _movieService.UpdateMovie(movies, true); + } + + var updatedCollection = _collectionService.UpdateCollection(collection); + update.Add(updatedCollection.ToResource()); + } + + return Accepted(update); + } + + private CollectionResource MapToResource(MovieCollection collection) + { + var resource = collection.ToResource(); + + foreach (var movie in _movieMetadataService.GetMoviesByCollectionTmdbId(collection.TmdbId)) + { + var movieResource = movie.ToResource(); + movieResource.Folder = _fileNameBuilder.GetMovieFolder(new Movie { Title = movie.Title, Year = movie.Year, ImdbId = movie.ImdbId, TmdbId = movie.TmdbId }); + + resource.Movies.Add(movieResource); + } + + return resource; + } + + [NonAction] + public void Handle(CollectionAddedEvent message) + { + BroadcastResourceChange(ModelAction.Created, MapToResource(message.Collection)); + } + + [NonAction] + public void Handle(CollectionEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Collection)); + } + + [NonAction] + public void Handle(CollectionDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.Collection)); + } + } +} diff --git a/src/Radarr.Api.V3/Collections/CollectionMovieResource.cs b/src/Radarr.Api.V3/Collections/CollectionMovieResource.cs new file mode 100644 index 000000000..d7cea6503 --- /dev/null +++ b/src/Radarr.Api.V3/Collections/CollectionMovieResource.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; + +namespace Radarr.Api.V3.Collections +{ + public class CollectionMovieResource + { + public int TmdbId { get; set; } + public string ImdbId { get; set; } + public string Title { get; set; } + public string CleanTitle { get; set; } + public string SortTitle { get; set; } + public string Overview { get; set; } + public int Runtime { get; set; } + public List Images { get; set; } + public int Year { get; set; } + public Ratings Ratings { get; set; } + public List Genres { get; set; } + public string Folder { get; set; } + } + + public static class CollectionMovieResourceMapper + { + public static CollectionMovieResource ToResource(this MovieMetadata model) + { + if (model == null) + { + return null; + } + + return new CollectionMovieResource + { + TmdbId = model.TmdbId, + Title = model.Title, + Overview = model.Overview, + SortTitle = model.SortTitle, + Images = model.Images, + ImdbId = model.ImdbId, + Ratings = model.Ratings, + Runtime = model.Runtime, + CleanTitle = model.CleanTitle, + Genres = model.Genres, + Year = model.Year + }; + } + } +} diff --git a/src/Radarr.Api.V3/Collections/CollectionResource.cs b/src/Radarr.Api.V3/Collections/CollectionResource.cs new file mode 100644 index 000000000..88b6b1bdf --- /dev/null +++ b/src/Radarr.Api.V3/Collections/CollectionResource.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; +using Radarr.Http.REST; + +namespace Radarr.Api.V3.Collections +{ + public class CollectionResource : RestResource + { + public CollectionResource() + { + Movies = new List(); + } + + public string Title { get; set; } + public string SortTitle { get; set; } + public int TmdbId { get; set; } + public List Images { get; set; } + public string Overview { get; set; } + public bool Monitored { get; set; } + public string RootFolderPath { get; set; } + public int QualityProfileId { get; set; } + public bool SearchOnAdd { get; set; } + public MovieStatusType MinimumAvailability { get; set; } + public List Movies { get; set; } + } + + public static class CollectionResourceMapper + { + public static CollectionResource ToResource(this MovieCollection model) + { + if (model == null) + { + return null; + } + + return new CollectionResource + { + Id = model.Id, + TmdbId = model.TmdbId, + Title = model.Title, + Overview = model.Overview, + SortTitle = model.SortTitle, + Monitored = model.Monitored, + Images = model.Images, + QualityProfileId = model.QualityProfileId, + RootFolderPath = model.RootFolderPath, + MinimumAvailability = model.MinimumAvailability, + SearchOnAdd = model.SearchOnAdd + }; + } + + public static List ToResource(this IEnumerable collections) + { + return collections.Select(ToResource).ToList(); + } + + public static MovieCollection ToModel(this CollectionResource resource) + { + if (resource == null) + { + return null; + } + + return new MovieCollection + { + Id = resource.Id, + Title = resource.Title, + TmdbId = resource.TmdbId, + SortTitle = resource.SortTitle, + Overview = resource.Overview, + Monitored = resource.Monitored, + QualityProfileId = resource.QualityProfileId, + RootFolderPath = resource.RootFolderPath, + SearchOnAdd = resource.SearchOnAdd, + MinimumAvailability = resource.MinimumAvailability + }; + } + + public static MovieCollection ToModel(this CollectionResource resource, MovieCollection collection) + { + var updatedmovie = resource.ToModel(); + + collection.ApplyChanges(updatedmovie); + + return collection; + } + } +} diff --git a/src/Radarr.Api.V3/Collections/CollectionUpdateCollectionResource.cs b/src/Radarr.Api.V3/Collections/CollectionUpdateCollectionResource.cs new file mode 100644 index 000000000..36fc06875 --- /dev/null +++ b/src/Radarr.Api.V3/Collections/CollectionUpdateCollectionResource.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Radarr.Api.V3.Collections +{ + public class CollectionUpdateCollectionResource + { + public int Id { get; set; } + public bool? Monitored { get; set; } + } +} diff --git a/src/Radarr.Api.V3/Collections/CollectionUpdateResource.cs b/src/Radarr.Api.V3/Collections/CollectionUpdateResource.cs new file mode 100644 index 000000000..05e269814 --- /dev/null +++ b/src/Radarr.Api.V3/Collections/CollectionUpdateResource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Radarr.Api.V3.Collections +{ + public class CollectionUpdateResource + { + public List Collections { get; set; } + public bool? MonitorMovies { get; set; } + } +} diff --git a/src/Radarr.Api.V3/ImportLists/ImportListMoviesResource.cs b/src/Radarr.Api.V3/ImportLists/ImportListMoviesResource.cs index c4c71bb42..687fc964e 100644 --- a/src/Radarr.Api.V3/ImportLists/ImportListMoviesResource.cs +++ b/src/Radarr.Api.V3/ImportLists/ImportListMoviesResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Core.ImportLists.ImportListMovies; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; using Radarr.Http.REST; namespace Radarr.Api.V3.ImportLists @@ -75,8 +76,8 @@ namespace Radarr.Api.V3.ImportLists Genres = model.MovieMetadata.Value.Genres, Ratings = model.MovieMetadata.Value.Ratings, YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId, - Studio = model.MovieMetadata.Value.Studio, - Collection = model.MovieMetadata.Value.Collection + Collection = new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId }, + Studio = model.MovieMetadata.Value.Studio }; } @@ -111,7 +112,7 @@ namespace Radarr.Api.V3.ImportLists Ratings = model.MovieMetadata.Value.Ratings, YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId, Studio = model.MovieMetadata.Value.Studio, - Collection = model.MovieMetadata.Value.Collection, + Collection = new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId }, Lists = new HashSet { model.ListId } }; } diff --git a/src/Radarr.Api.V3/ImportLists/ImportListResource.cs b/src/Radarr.Api.V3/ImportLists/ImportListResource.cs index 08019e064..a97f3007d 100644 --- a/src/Radarr.Api.V3/ImportLists/ImportListResource.cs +++ b/src/Radarr.Api.V3/ImportLists/ImportListResource.cs @@ -7,7 +7,7 @@ namespace Radarr.Api.V3.ImportLists { public bool Enabled { get; set; } public bool EnableAuto { get; set; } - public bool ShouldMonitor { get; set; } + public MonitorTypes Monitor { get; set; } public string RootFolderPath { get; set; } public int QualityProfileId { get; set; } public bool SearchOnAdd { get; set; } @@ -29,7 +29,7 @@ namespace Radarr.Api.V3.ImportLists resource.Enabled = definition.Enabled; resource.EnableAuto = definition.EnableAuto; - resource.ShouldMonitor = definition.ShouldMonitor; + resource.Monitor = definition.Monitor; resource.SearchOnAdd = definition.SearchOnAdd; resource.RootFolderPath = definition.RootFolderPath; resource.QualityProfileId = definition.ProfileId; @@ -51,7 +51,7 @@ namespace Radarr.Api.V3.ImportLists definition.Enabled = resource.Enabled; definition.EnableAuto = resource.EnableAuto; - definition.ShouldMonitor = resource.ShouldMonitor; + definition.Monitor = resource.Monitor; definition.SearchOnAdd = resource.SearchOnAdd; definition.RootFolderPath = resource.RootFolderPath; definition.ProfileId = resource.QualityProfileId; diff --git a/src/Radarr.Api.V3/Movies/MovieResource.cs b/src/Radarr.Api.V3/Movies/MovieResource.cs index 605c265c9..047349e37 100644 --- a/src/Radarr.Api.V3/Movies/MovieResource.cs +++ b/src/Radarr.Api.V3/Movies/MovieResource.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Translations; using NzbDrone.Core.Parser; using Radarr.Api.V3.MovieFiles; @@ -140,7 +141,7 @@ namespace Radarr.Api.V3.Movies MovieFile = movieFile, YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId, Studio = model.MovieMetadata.Value.Studio, - Collection = model.MovieMetadata.Value.Collection, + Collection = new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId }, Popularity = model.MovieMetadata.Value.Popularity }; } diff --git a/yarn.lock b/yarn.lock index 61d674c90..2830d5eff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,12 +16,7 @@ dependencies: "@babel/highlight" "^7.16.7" -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4", "@babel/compat-data@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" - integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== - -"@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.7": +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== @@ -56,19 +51,10 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.0" -"@babel/generator@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.3.tgz#a2c30b0c4f89858cb87050c3ffdfd36bdf443200" - integrity sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg== - dependencies: - "@babel/types" "^7.17.0" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/generator@^7.17.7": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad" - integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== +"@babel/generator@^7.17.7", "@babel/generator@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.9.tgz#f4af9fd38fa8de143c29fce3f71852406fc1e2fc" + integrity sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ== dependencies: "@babel/types" "^7.17.0" jsesc "^2.5.1" @@ -89,17 +75,7 @@ "@babel/helper-explode-assignable-expression" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" - integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== - dependencies: - "@babel/compat-data" "^7.16.4" - "@babel/helper-validator-option" "^7.16.7" - browserslist "^4.17.5" - semver "^6.3.0" - -"@babel/helper-compilation-targets@^7.17.7": +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== @@ -110,14 +86,14 @@ semver "^6.3.0" "@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": - version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" - integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz#71835d7fb9f38bd9f1378e40a4c0902fdc2ea49d" + integrity sha512-kUjip3gruz6AJKOq5i3nC6CoCEEF/oHH3cp6tOZhB+IyyyPyW0g1Gfsxn3mkk6S08pIA2y8GQh609v9G/5sHVQ== dependencies: "@babel/helper-annotate-as-pure" "^7.16.7" "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-member-expression-to-functions" "^7.17.7" "@babel/helper-optimise-call-expression" "^7.16.7" "@babel/helper-replace-supers" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" @@ -158,21 +134,13 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-function-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" - integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== +"@babel/helper-function-name@^7.16.7", "@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== dependencies: - "@babel/helper-get-function-arity" "^7.16.7" "@babel/template" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/helper-get-function-arity@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" - integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== - dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/helper-hoist-variables@^7.16.7": version "7.16.7" @@ -181,12 +149,12 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-member-expression-to-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz#42b9ca4b2b200123c3b7e726b0ae5153924905b0" - integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q== +"@babel/helper-member-expression-to-functions@^7.16.7", "@babel/helper-member-expression-to-functions@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.17.0" "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": version "7.16.7" @@ -195,21 +163,7 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-module-transforms@^7.16.7": - version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.6.tgz#3c3b03cc6617e33d68ef5a27a67419ac5199ccd0" - integrity sha512-2ULmRdqoOMpdvkbT8jONrZML/XALfzxlb052bldftkicAUy8AxSCkD5trDPQcwHNmolcl7wP6ehNqMlyUw6AaA== - dependencies: - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/helper-validator-identifier" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" - "@babel/types" "^7.17.0" - -"@babel/helper-module-transforms@^7.17.7": +"@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== @@ -255,13 +209,6 @@ "@babel/traverse" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/helper-simple-access@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" - integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== - dependencies: - "@babel/types" "^7.16.7" - "@babel/helper-simple-access@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" @@ -304,32 +251,27 @@ "@babel/types" "^7.16.8" "@babel/helpers@^7.17.8": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.8.tgz#288450be8c6ac7e4e44df37bcc53d345e07bc106" - integrity sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" + integrity sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q== dependencies: "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" + "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" "@babel/highlight@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" - integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" + integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== dependencies: "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.16.7", "@babel/parser@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0" - integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA== - -"@babel/parser@^7.17.8": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" - integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== +"@babel/parser@^7.16.7", "@babel/parser@^7.17.8", "@babel/parser@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef" + integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" @@ -765,9 +707,9 @@ babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-commonjs@^7.16.8": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz#d86b217c8e45bb5f2dbc11eefc8eab62cf980d19" - integrity sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.9.tgz#274be1a2087beec0254d4abd4d86e52442e1e5b6" + integrity sha512-2TBFd/r2I6VlYn0YRTz2JdazS+FoUuQ2rIFHoAxtyP/0G3D82SBLaRq9rnUkpqlLg03Byfl/+M32mpxjO6KaPw== dependencies: "@babel/helper-module-transforms" "^7.17.7" "@babel/helper-plugin-utils" "^7.16.7" @@ -863,11 +805,11 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-regenerator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" - integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz#0a33c3a61cf47f45ed3232903683a0afd2d3460c" + integrity sha512-Lc2TfbxR1HOyn/c6b4Y/b6NHoTb67n/IoWLxTu4kC7h4KQnWlhCq2S8Tx0t2SVvv5Uu87Hs+6JEJ5kt2tYGylQ== dependencies: - regenerator-transform "^0.14.2" + regenerator-transform "^0.15.0" "@babel/plugin-transform-reserved-words@^7.16.7": version "7.16.7" @@ -1031,9 +973,9 @@ "@babel/plugin-transform-react-pure-annotations" "^7.16.7" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" - integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== dependencies: regenerator-runtime "^0.13.4" @@ -1046,18 +988,18 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" - integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.9.tgz#1f9b207435d9ae4a8ed6998b2b82300d83c37a0d" + integrity sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw== dependencies: "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.3" + "@babel/generator" "^7.17.9" "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" "@babel/helper-hoist-variables" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.3" + "@babel/parser" "^7.17.9" "@babel/types" "^7.17.0" debug "^4.1.0" globals "^11.1.0" @@ -1079,14 +1021,14 @@ minimist "^1.2.0" "@discoveryjs/json-ext@^0.5.0": - version "0.5.6" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" - integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA== + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== "@eslint/eslintrc@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" - integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== + version "1.2.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae" + integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1151,9 +1093,9 @@ integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@jridgewell/resolve-uri@^3.0.3": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" - integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + version "3.0.6" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz#4ac237f4dabc8dd93330386907b97591801f7352" + integrity sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw== "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.11" @@ -1161,9 +1103,9 @@ integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== "@jridgewell/trace-mapping@^0.3.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" - integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1214,9 +1156,9 @@ fastq "^1.6.0" "@react-dnd/asap@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651" - integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab" + integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg== "@react-dnd/invariant@^2.0.0": version "2.0.0" @@ -1354,9 +1296,9 @@ integrity sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w== "@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/json5@^0.0.29": version "0.0.29" @@ -1364,9 +1306,9 @@ integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= "@types/lodash@^4.14.159": - version "4.14.179" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5" - integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w== + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== "@types/minimatch@*": version "3.0.5" @@ -1379,14 +1321,14 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@*": - version "17.0.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" - integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" + integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== "@types/node@^12.12.54": - version "12.20.46" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.46.tgz#7e49dee4c54fd19584e6a9e0da5f3dc2e9136bc7" - integrity sha512-cPjLXj8d6anFPzFvOPxS3fvly3Shm5nTfl6g8X5smexixbuGUf7hfr21J5tX9JW+UPStp/5P5R8qrKL5IyVJ+A== + version "12.20.48" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.48.tgz#55f70bd432b6515828c0298689776861b90ca4fa" + integrity sha512-4kxzqkrpwYtn6okJUcb2lfUu9ilnb3yhUOH6qX3nug8D2DupZ2drIkff2yJzYcNJVl3begnlcaBJ7tqiTTzjnQ== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -1399,9 +1341,9 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prop-types@*": - version "15.7.4" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" - integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== "@types/qs@*": version "6.9.7" @@ -1414,9 +1356,9 @@ integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== "@types/react-redux@^7.1.16": - version "7.1.23" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.23.tgz#3c2bb1bcc698ae69d70735f33c5a8e95f41ac528" - integrity sha512-D02o3FPfqQlfu2WeEYwh3x2otYd2Dk1o8wAfsA0B1C2AJEFxE663Ozu7JzuWbznGgW248NaOF6wsqCGNq9d3qw== + version "7.1.24" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" + integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -1424,9 +1366,9 @@ redux "^4.0.0" "@types/react@*": - version "17.0.39" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.39.tgz#d0f4cde092502a6db00a1cded6e6bf2abb7633ce" - integrity sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug== + version "18.0.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.6.tgz#30206c3830af6ce8639b91ace5868bc2d3d1d96c" + integrity sha512-bPqwzJRzKtfI0mVYr5R+1o9BOE8UEXefwc1LwcBtfnaAn6OoqMhLa/91VA8aeWfDPJt1kHvYKI8RHcQybZLHHA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -1665,9 +1607,9 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d" - integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -1754,12 +1696,12 @@ archiver-utils@^2.1.0: readable-stream "^2.0.0" archiver@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" - integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== + version "5.3.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" + integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== dependencies: archiver-utils "^2.1.0" - async "^3.2.0" + async "^3.2.3" buffer-crc32 "^0.2.1" readable-stream "^3.6.0" readdir-glob "^1.0.0" @@ -1799,7 +1741,7 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-includes@^3.1.3, array-includes@^3.1.4: +array-includes@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== @@ -1838,22 +1780,24 @@ array-unique@^0.3.2: integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= array.prototype.flat@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" - integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + version "1.3.0" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" + integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.19.0" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" array.prototype.flatmap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" - integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz#a7e8ed4225f4788a70cd910abcf0791e76a5534f" + integrity sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.19.0" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" arrify@^1.0.1: version "1.0.1" @@ -1876,13 +1820,13 @@ astral-regex@^2.0.0: integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" -async@^3.2.0: +async@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== @@ -2049,20 +1993,20 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -browserslist@^4.14.5, browserslist@^4.16.3, browserslist@^4.17.5, browserslist@^4.19.1: - version "4.19.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.3.tgz#29b7caad327ecf2859485f696f9604214bedd383" - integrity sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg== +browserslist@^4.14.5, browserslist@^4.16.3, browserslist@^4.17.5, browserslist@^4.20.2: + version "4.20.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" + integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== dependencies: - caniuse-lite "^1.0.30001312" - electron-to-chromium "^1.4.71" + caniuse-lite "^1.0.30001317" + electron-to-chromium "^1.4.84" escalade "^3.1.1" node-releases "^2.0.2" picocolors "^1.0.0" @@ -2157,10 +2101,10 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001312: - version "1.0.30001313" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001313.tgz#a380b079db91621e1b7120895874e2fd62ed2e2f" - integrity sha512-rI1UN0koZUiKINjysQDuRi2VeSCce3bYJNmDcj3PIKREiAmjakugBul1QSkg/fPrlULYl6oWfGg3PbgOSY9X4Q== +caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001317: + version "1.0.30001332" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd" + integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== capture-exit@^2.0.0: version "2.0.0" @@ -2217,7 +2161,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.3.1: +classnames@2.3.1, classnames@^2.2.5: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== @@ -2415,11 +2359,11 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-compat@^3.20.2, core-js-compat@^3.21.0: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" - integrity sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g== + version "3.22.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.2.tgz#eec621eb276518efcf718d0a6d9d042c3d0cad48" + integrity sha512-Fns9lU06ZJ07pdfmPMu7OnkIKGPKDzXKIiuGlSvHHapwqMUF2QnnsWwtueFZtSyZEilP0o6iUeHQwpn7LxtLUw== dependencies: - browserslist "^4.19.1" + browserslist "^4.20.2" semver "7.0.0" core-js@3.12.1: @@ -2474,12 +2418,9 @@ cpy@^8.1.2: p-map "^3.0.0" crc-32@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.1.tgz#436d2bcaad27bcb6bd073a2587139d3024a16460" - integrity sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w== - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.3.1" + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== crc32-stream@^4.0.2: version "4.0.2" @@ -2536,20 +2477,20 @@ css-loader@6.5.1: semver "^7.3.5" css-select@^4.1.3: - version "4.2.1" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" - integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== dependencies: boolbase "^1.0.0" - css-what "^5.1.0" - domhandler "^4.3.0" + css-what "^6.0.1" + domhandler "^4.3.1" domutils "^2.8.0" nth-check "^2.0.1" -css-what@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssesc@^3.0.0: version "3.0.0" @@ -2580,14 +2521,7 @@ debug@^3.1.0, debug@^3.1.1, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - -debug@^4.3.2, debug@^4.3.3: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2630,11 +2564,12 @@ deep-is@^0.1.3: integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: - object-keys "^1.0.12" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" define-property@^0.2.5: version "0.2.5" @@ -2753,23 +2688,23 @@ dom-css@^2.0.0: "@babel/runtime" "^7.1.2" dom-serializer@^1.0.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== dependencies: domelementtype "^2.0.1" domhandler "^4.2.0" entities "^2.0.0" domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" - integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: domelementtype "^2.2.0" @@ -2790,10 +2725,10 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -electron-to-chromium@^1.4.71: - version "1.4.76" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.76.tgz#a0494baedaf51094b1c172999919becd9975a934" - integrity sha512-3Vftv7cenJtQb+k00McEBZ2vVmZ/x+HEF7pcZONZIkOsESqAqVuACmBxMv0JhzX7u0YltU0vSqRqgBSTAhFUjA== +electron-to-chromium@^1.4.84: + version "1.4.118" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.118.tgz#2d917c71712dac9652cc01af46c7d0bd51552974" + integrity sha512-maZIKjnYDvF7Fs35nvVcyr44UcKNwybr93Oba2n3HkKDFAtk0svERkLN/HyczJDS3Fo4wU9th9fUQd09ZLtj1w== element-class@0.2.2: version "0.2.2" @@ -2825,6 +2760,11 @@ enhanced-resolve@^5.8.3: graceful-fs "^4.2.4" tapable "^2.2.0" +enquire.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/enquire.js/-/enquire.js-2.1.6.tgz#3e8780c9b8b835084c3f60e166dbc3c2a3c89814" + integrity sha1-PoeAybi4NQhMP2DhZtvDwqPImBQ= + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -2849,10 +2789,10 @@ error@^7.0.0: dependencies: string-template "~0.2.1" -es-abstract@^1.19.0, es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== +es-abstract@^1.19.1, es-abstract@^1.19.2: + version "1.19.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.5.tgz#a2cb01eb87f724e815b278b0dd0d00f36ca9a7f1" + integrity sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" @@ -2860,15 +2800,15 @@ es-abstract@^1.19.0, es-abstract@^1.19.1: get-intrinsic "^1.1.1" get-symbol-description "^1.0.0" has "^1.0.3" - has-symbols "^1.0.2" + has-symbols "^1.0.3" internal-slot "^1.0.3" is-callable "^1.2.4" - is-negative-zero "^2.0.1" + is-negative-zero "^2.0.2" is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" + is-shared-array-buffer "^1.0.2" is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" + is-weakref "^1.0.2" + object-inspect "^1.12.0" object-keys "^1.1.1" object.assign "^4.1.2" string.prototype.trimend "^1.0.4" @@ -2880,6 +2820,13 @@ es-module-lexer@^0.9.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -3177,11 +3124,6 @@ execall@^2.0.0: dependencies: clone-regexp "^2.1.0" -exit-on-epipe@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" - integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== - expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -3416,9 +3358,9 @@ for-in@^1.0.2: integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= fraction.js@^4.0.13: - version "4.1.3" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.3.tgz#be65b0f20762ef27e1e793860bc2dfb716e99e65" - integrity sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg== + version "4.2.0" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== fragment-cache@^0.2.1: version "0.2.1" @@ -3433,9 +3375,9 @@ fs-constants@^1.0.0: integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== fs-extra@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" - integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -3456,6 +3398,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + fuse.js@6.4.6: version "6.4.6" resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.4.6.tgz#62f216c110e5aa22486aff20be7896d19a059b79" @@ -3581,9 +3528,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.12.1" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" - integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== + version "13.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" + integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== dependencies: type-fest "^0.20.2" @@ -3626,9 +3573,9 @@ good-listener@^1.2.2: delegate "^3.1.2" graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== gud@^1.0.0: version "1.0.0" @@ -3648,9 +3595,9 @@ has-ansi@^2.0.0: ansi-regex "^2.0.0" has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== has-flag@^3.0.0: version "3.0.0" @@ -3669,6 +3616,13 @@ has-glob@^1.0.0: dependencies: is-glob "^3.0.0" +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -3769,9 +3723,9 @@ html-minifier-terser@^5.0.1: terser "^4.6.3" html-tags@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" - integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" + integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== html-webpack-plugin@5.3.1: version "5.3.1" @@ -3795,9 +3749,9 @@ htmlparser2@^6.1.0: entities "^2.0.0" http-parser-js@>=0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.5.tgz#d7c30d5d3c90d865b4a2e870181f9d6f22ac7ac5" - integrity sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA== + version "0.5.6" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.6.tgz#2e02406ab2df8af8a7abfba62e0da01c62b95afd" + integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== https-browserify@1.0.0: version "1.0.0" @@ -3967,9 +3921,9 @@ is-callable@^1.1.4, is-callable@^1.2.4: integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.0, is-core-module@^2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" - integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== dependencies: has "^1.0.3" @@ -4048,15 +4002,15 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" -is-negative-zero@^2.0.1: +is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== dependencies: has-tostringtag "^1.0.0" @@ -4112,10 +4066,12 @@ is-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" is-stream@^2.0.0: version "2.0.1" @@ -4136,7 +4092,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-weakref@^1.0.1: +is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== @@ -4277,6 +4233,13 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" + integrity sha1-tje9O6nqvhIsg+lyBIOusQ0skEo= + dependencies: + string-convert "^0.2.0" + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -4285,11 +4248,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== jsonc-parser@^3.0.0: version "3.0.0" @@ -4311,11 +4272,11 @@ jsonparse@^1.2.0: integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= "jsx-ast-utils@^2.4.1 || ^3.0.0": - version "3.2.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" - integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== + version "3.2.2" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.2.tgz#6ab1e52c71dfc0c0707008a91729a9491fe9f76c" + integrity sha512-HDAyJ4MNQBboGpUnHAVUNJs6X0lh058s6FuixsFGP7MgJYpD6Vasd6nzSG5iIfXu1zAYlHJ/zsOKNlrenTUBnw== dependencies: - array-includes "^3.1.3" + array-includes "^3.1.4" object.assign "^4.1.2" junk@^3.1.0: @@ -4405,9 +4366,9 @@ livereload-js@^2.3.0: integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw== loader-runner@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" - integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== loader-utils@^1.4.0: version "1.4.0" @@ -4629,24 +4590,24 @@ micromatch@^3.1.10: to-regex "^3.0.2" micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.2" + picomatch "^2.3.1" -mime-db@1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.27: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.51.0" + mime-db "1.52.0" mime@~2.5.2: version "2.5.2" @@ -4703,10 +4664,10 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mixin-deep@^1.2.0: version "1.3.2" @@ -4717,11 +4678,11 @@ mixin-deep@^1.2.0: is-extendable "^1.0.1" mkdirp@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== dependencies: - minimist "^1.2.5" + minimist "^1.2.6" mobile-detect@1.4.5: version "1.4.5" @@ -4754,9 +4715,9 @@ ms@^2.1.1: integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== nanoid@^3.1.22, nanoid@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== nanomatch@^1.2.9: version "1.2.13" @@ -4786,9 +4747,9 @@ neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" - integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== + version "2.1.1" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5" + integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw== no-case@^3.0.4: version "3.0.4" @@ -4811,9 +4772,9 @@ node-int64@^0.4.0: integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= node-releases@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" - integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" + integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw== normalize-package-data@^2.5.0: version "2.5.0" @@ -4888,7 +4849,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.11.0, object-inspect@^1.9.0: +object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== @@ -4901,7 +4862,7 @@ object-is@^1.0.1: call-bind "^1.0.2" define-properties "^1.1.3" -object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -5190,7 +5151,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -5334,15 +5295,7 @@ postcss-safe-parser@^6.0.0: resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.9: - version "6.0.9" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" - integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-selector-parser@^6.0.6: +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: version "6.0.10" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== @@ -5398,16 +5351,7 @@ postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.1.6: - version "8.4.7" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.7.tgz#f99862069ec4541de386bf57f5660a6c7a0875a8" - integrity sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A== - dependencies: - nanoid "^3.3.1" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.2.15, postcss@^8.3.11, postcss@^8.4.12: +postcss@^8.1.6, postcss@^8.2.15, postcss@^8.3.11, postcss@^8.4.12: version "8.4.12" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== @@ -5434,11 +5378,6 @@ pretty-error@^2.1.1: lodash "^4.17.20" renderkid "^2.0.4" -printj@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.3.1.tgz#9af6b1d55647a1587ac44f4c1654a4b95b8e12cb" - integrity sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -5730,6 +5669,17 @@ react-side-effect@^1.0.2: dependencies: shallowequal "^1.0.1" +react-slick@0.28.1: + version "0.28.1" + resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.28.1.tgz#12c18d991b59432df9c3757ba540a227b3fb85b9" + integrity sha512-JwRQXoWGJRbUTE7eZI1rGIHaXX/4YuwX6gn7ulfvUZ4vFDVQAA25HcsHSYaUiRCduTr6rskyIuyPMpuG6bbluw== + dependencies: + classnames "^2.2.5" + enquire.js "^2.1.6" + json2mq "^0.2.0" + lodash.debounce "^4.0.8" + resize-observer-polyfill "^1.5.0" + react-slider@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-1.1.4.tgz#08b55f9be3e04cc10ae00cc3aedb6891dffe9bf3" @@ -5880,9 +5830,9 @@ redux@4.1.0: "@babel/runtime" "^7.9.2" redux@^4.0.0, redux@^4.0.5: - version "4.1.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" - integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== dependencies: "@babel/runtime" "^7.9.2" @@ -5908,10 +5858,10 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== +regenerator-transform@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" + integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== dependencies: "@babel/runtime" "^7.8.4" @@ -5924,12 +5874,13 @@ regex-not@^1.0.0, regex-not@^1.0.2: safe-regex "^1.1.0" regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" - integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" + functions-have-names "^1.2.2" regexpp@^3.2.0: version "3.2.0" @@ -6011,7 +5962,7 @@ reselect@4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== -resize-observer-polyfill@^1.4.1: +resize-observer-polyfill@^1.4.1, resize-observer-polyfill@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== @@ -6207,9 +6158,9 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" @@ -6292,6 +6243,11 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +slick-carousel@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/slick-carousel/-/slick-carousel-1.8.1.tgz#a4bfb29014887bb66ce528b90bd0cda262cc8f8d" + integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -6425,6 +6381,11 @@ streamqueue@1.1.2: isstream "^0.1.2" readable-stream "^2.3.3" +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" + integrity sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c= + string-template@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" @@ -6688,9 +6649,9 @@ terser@^4.6.3: source-map-support "~0.5.12" terser@^5.7.2: - version "5.12.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.0.tgz#728c6bff05f7d1dcb687d8eace0644802a9dae8a" - integrity sha512-R3AUhNBGWiFc77HXag+1fXpAxTAFRQTJemlJKjAgD9r8xXTpjNKqIXwHM/o7Rh+O0kUJtS3WQVdBeMKFk5sw9A== + version "5.12.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c" + integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ== dependencies: acorn "^8.5.0" commander "^2.20.0" @@ -6820,13 +6781,13 @@ trim-newlines@^3.0.0: integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== tsconfig-paths@^3.12.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.0.tgz#4fcc48f9ccea8826c41b9ca093479de7f5018976" - integrity sha512-cg/1jAZoL57R39+wiw4u/SCC6Ic9Q5NqjBOb+9xISedOYurfog9ZNmKJSxAnb2m/5Bq4lE9lhUcau33Ml8DM0g== + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" - minimist "^1.2.0" + minimist "^1.2.6" strip-bom "^3.0.0" tslib@^1.9.3: @@ -6834,10 +6795,10 @@ tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.0.0, tslib@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -6962,17 +6923,19 @@ url-parse@^1.4.3: requires-port "^1.0.0" use-callback-ref@^1.2.1: - version "1.2.5" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" - integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + dependencies: + tslib "^2.0.0" use-sidecar@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.5.tgz#ffff2a17c1df42e348624b699ba6e5c220527f2b" - integrity sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== dependencies: detect-node-es "^1.1.0" - tslib "^1.9.3" + tslib "^2.0.0" use@^3.1.0: version "3.1.1" @@ -7274,9 +7237,9 @@ yargs-parser@^21.0.0: integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== yargs@^17.3.1: - version "17.4.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.0.tgz#9fc9efc96bd3aa2c1240446af28499f0e7593d00" - integrity sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA== + version "17.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284" + integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g== dependencies: cliui "^7.0.2" escalade "^3.1.1"