Newznab Responses for Caps and Movie Search (rough)

This commit is contained in:
Qstick
2020-10-21 01:47:20 -04:00
parent 84cbfe870f
commit cfb1a80c58
46 changed files with 526 additions and 1226 deletions

View File

@@ -0,0 +1,55 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.indexers,
(value, indexers) => {
const values = indexers.items.map(({ id, name }) => {
return {
key: id,
value: name
};
});
return {
value,
values
};
}
);
}
class IndexersSelectInputConnector extends Component {
onChange = ({ name, value }) => {
this.props.onChange({ name, value });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexersSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
indexerIds: PropTypes.number.isRequired,
value: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps)(IndexersSelectInputConnector);

View File

@@ -26,34 +26,16 @@ function MovieIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Monitored/Status
Status
</SortMenuItem>
<SortMenuItem
name="sortTitle"
name="name"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Title')}
</SortMenuItem>
<SortMenuItem
name="studio"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Studio')}
</SortMenuItem>
<SortMenuItem
name="qualityProfileId"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('QualityProfile')}
{translate('Name')}
</SortMenuItem>
<SortMenuItem
@@ -66,66 +48,21 @@ function MovieIndexSortMenu(props) {
</SortMenuItem>
<SortMenuItem
name="year"
name="protocol"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Year')}
{'Protocol'}
</SortMenuItem>
<SortMenuItem
name="inCinemas"
name="privacy"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('InCinemas')}
</SortMenuItem>
<SortMenuItem
name="physicalRelease"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('PhysicalRelease')}
</SortMenuItem>
<SortMenuItem
name="digitalRelease"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('DigitalRelease')}
</SortMenuItem>
<SortMenuItem
name="path"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Path')}
</SortMenuItem>
<SortMenuItem
name="sizeOnDisk"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('SizeOnDisk')}
</SortMenuItem>
<SortMenuItem
name="certification"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Certification')}
{'Privacy'}
</SortMenuItem>
</MenuContent>
</SortMenu>

View File

@@ -4,16 +4,16 @@ import Label from 'Components/Label';
function CapabilitiesLabel(props) {
const {
supportsBooks,
supportsMovies,
supportsMusic,
supportsTv
} = props;
movieSearchAvailable,
tvSearchAvailable,
musicSearchAvailable,
bookSearchAvailable
} = props.capabilities;
return (
<span>
{
supportsBooks ?
bookSearchAvailable ?
<Label>
{'Books'}
</Label> :
@@ -21,7 +21,7 @@ function CapabilitiesLabel(props) {
}
{
supportsMovies ?
movieSearchAvailable ?
<Label>
{'Movies'}
</Label> :
@@ -29,7 +29,7 @@ function CapabilitiesLabel(props) {
}
{
supportsMusic ?
musicSearchAvailable ?
<Label>
{'Music'}
</Label> :
@@ -37,7 +37,7 @@ function CapabilitiesLabel(props) {
}
{
supportsTv ?
tvSearchAvailable ?
<Label>
{'TV'}
</Label> :
@@ -45,7 +45,7 @@ function CapabilitiesLabel(props) {
}
{
!supportsTv && !supportsMusic && !supportsMovies && !supportsBooks ?
!tvSearchAvailable && !musicSearchAvailable && !movieSearchAvailable && !bookSearchAvailable ?
<Label>
{'None'}
</Label> :
@@ -56,10 +56,7 @@ function CapabilitiesLabel(props) {
}
CapabilitiesLabel.propTypes = {
supportsTv: PropTypes.bool.isRequired,
supportsBooks: PropTypes.bool.isRequired,
supportsMusic: PropTypes.bool.isRequired,
supportsMovies: PropTypes.bool.isRequired
capabilities: PropTypes.object.isRequired
};
export default CapabilitiesLabel;

View File

@@ -64,10 +64,7 @@ class MovieIndexRow extends Component {
protocol,
privacy,
added,
supportsTv,
supportsBooks,
supportsMusic,
supportsMovies,
capabilities,
columns,
isMovieEditorActive,
isSelected,
@@ -175,10 +172,7 @@ class MovieIndexRow extends Component {
className={styles[column.name]}
>
<CapabilitiesLabel
supportsBooks={supportsBooks}
supportsMovies={supportsMovies}
supportsMusic={supportsMusic}
supportsTv={supportsTv}
capabilities={capabilities}
/>
</VirtualTableRowCell>
);
@@ -243,10 +237,7 @@ MovieIndexRow.propTypes = {
enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired,
supportsTv: PropTypes.bool.isRequired,
supportsBooks: PropTypes.bool.isRequired,
supportsMusic: PropTypes.bool.isRequired,
supportsMovies: PropTypes.bool.isRequired,
capabilities: PropTypes.object.isRequired,
added: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -0,0 +1,6 @@
.message {
margin-top: 10px;
margin-bottom: 30px;
text-align: center;
font-size: 20px;
}

View File

@@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
import React from 'react';
import translate from 'Utilities/String/translate';
import styles from './NoSearchResults.css';
function NoSearchResults(props) {
const { totalItems } = props;
if (totalItems > 0) {
return (
<div>
<div className={styles.message}>
{translate('AllIndexersHiddenDueToFilter')}
</div>
</div>
);
}
return (
<div>
<div className={styles.message}>
No search results found, try performing a new search below.
</div>
</div>
);
}
NoSearchResults.propTypes = {
totalItems: PropTypes.number.isRequired
};
export default NoSearchResults;

View File

@@ -3,6 +3,11 @@
min-width: 150px;
}
.indexerContainer {
margin-right: 20px;
min-width: 250px;
}
.buttonContainer {
display: flex;
justify-content: flex-end;

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector';
import TextInput from 'Components/Form/TextInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
@@ -15,7 +16,8 @@ class SearchFooter extends Component {
this.state = {
searchingReleases: false,
searchQuery: ''
searchQuery: '',
indexerIds: []
};
}
@@ -40,7 +42,7 @@ class SearchFooter extends Component {
}
onSearchPress = () => {
this.props.onSearchPress(this.state.searchQuery);
this.props.onSearchPress(this.state.searchQuery, this.state.indexerIds);
}
//
@@ -52,7 +54,8 @@ class SearchFooter extends Component {
} = this.props;
const {
searchQuery
searchQuery,
indexerIds
} = this.state;
return (
@@ -67,6 +70,16 @@ class SearchFooter extends Component {
/>
</div>
<div className={styles.indexerContainer}>
<IndexersSelectInputConnector
name='indexerIds'
placeholder='Indexers'
value={indexerIds}
isDisabled={isFetching}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<div className={styles.buttons}>

View File

@@ -11,13 +11,13 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
import NoSearchResults from './NoSearchResults';
import SearchFooter from './SearchFooter.js';
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
import styles from './SearchIndex.css';
@@ -126,9 +126,8 @@ class SearchIndex extends Component {
this.setState({ jumpToCharacter });
}
onSearchPress = (query) => {
console.log('index', query);
this.props.onSearchPress({ query });
onSearchPress = (query, indexerIds) => {
this.props.onSearchPress({ query, indexerIds });
}
onKeyUp = (event) => {
@@ -175,6 +174,8 @@ class SearchIndex extends Component {
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems;
console.log(hasNoIndexer);
return (
<PageContent>
<PageToolbar>
@@ -246,8 +247,8 @@ class SearchIndex extends Component {
}
{
!error && isPopulated && !items.length &&
<NoIndexer totalItems={totalItems} />
!error && !isFetching && !items.length &&
<NoSearchResults totalItems={totalItems} />
}
</PageContentBody>

View File

@@ -95,25 +95,7 @@ export const defaultState = {
],
sortPredicates: {
...sortPredicates,
studio: function(item) {
const studio = item.studio;
return studio ? studio.toLowerCase() : '';
},
collection: function(item) {
const { collection ={} } = item;
return collection.name;
},
ratings: function(item) {
const { ratings = {} } = item;
return ratings.value;
}
...sortPredicates
},
selectedFilterKey: 'all',
@@ -122,12 +104,6 @@ export const defaultState = {
filterPredicates,
filterBuilderProps: [
{
name: 'monitored',
label: translate('Monitored'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'title',
label: 'Indexer Name',

View File

@@ -1,11 +1,10 @@
import _ from 'lodash';
import { createAction } from 'redux-actions';
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
// import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
import padNumber from 'Utilities/Number/padNumber';
import translate from 'Utilities/String/translate';
import { updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler';
@@ -24,123 +23,12 @@ export const filters = [
key: 'all',
label: translate('All'),
filters: []
},
{
key: 'monitored',
label: translate('MonitoredOnly'),
filters: [
{
key: 'monitored',
value: true,
type: filterTypes.EQUAL
}
]
},
{
key: 'unmonitored',
label: translate('Unmonitored'),
filters: [
{
key: 'monitored',
value: false,
type: filterTypes.EQUAL
}
]
},
{
key: 'missing',
label: translate('Missing'),
filters: [
{
key: 'monitored',
value: true,
type: filterTypes.EQUAL
},
{
key: 'hasFile',
value: false,
type: filterTypes.EQUAL
}
]
},
{
key: 'wanted',
label: translate('Wanted'),
filters: [
{
key: 'monitored',
value: true,
type: filterTypes.EQUAL
},
{
key: 'hasFile',
value: false,
type: filterTypes.EQUAL
},
{
key: 'isAvailable',
value: true,
type: filterTypes.EQUAL
}
]
},
{
key: 'cutoffunmet',
label: translate('CutoffUnmet'),
filters: [
{
key: 'monitored',
value: true,
type: filterTypes.EQUAL
},
{
key: 'hasFile',
value: true,
type: filterTypes.EQUAL
},
{
key: 'qualityCutoffNotMet',
value: true,
type: filterTypes.EQUAL
}
]
}
];
export const filterPredicates = {
added: function(item, filterValue, type) {
return dateFilterPredicate(item.added, filterValue, type);
},
collection: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { collection } = item;
return predicate(collection ? collection.name : '', filterValue);
},
inCinemas: function(item, filterValue, type) {
return dateFilterPredicate(item.inCinemas, filterValue, type);
},
physicalRelease: function(item, filterValue, type) {
return dateFilterPredicate(item.physicalRelease, filterValue, type);
},
digitalRelease: function(item, filterValue, type) {
return dateFilterPredicate(item.digitalRelease, filterValue, type);
},
ratings: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
return predicate(item.ratings.value * 10, filterValue);
},
qualityCutoffNotMet: function(item) {
const { movieFile = {} } = item;
return movieFile.qualityCutoffNotMet;
}
};
@@ -165,33 +53,6 @@ export const sortPredicates = {
}
return result;
},
movieStatus: function(item) {
let result = 0;
let qualityName = '';
const hasMovieFile = !!item.movieFile;
if (item.isAvailable) {
result++;
}
if (item.monitored) {
result += 2;
}
if (hasMovieFile) {
// TODO: Consider Quality Weight for Sorting within status of hasMovie
if (item.movieFile.qualityCutoffNotMet) {
result += 4;
} else {
result += 8;
}
qualityName = item.movieFile.quality.quality.name;
}
return padNumber(result.toString(), 2) + qualityName;
}
};
@@ -205,7 +66,7 @@ export const defaultState = {
isSaving: false,
saveError: null,
items: [],
sortKey: 'sortTitle',
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
pendingChanges: {}
};

View File

@@ -1,74 +0,0 @@
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'movieTitles';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_MOVIE_TITLES = 'movieTitles/fetchMovieTitles';
//
// Action Creators
export const fetchMovieTitles = createThunk(FETCH_MOVIE_TITLES);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_MOVIE_TITLES]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
url: '/alttitle',
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
}));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
}, defaultState, section);