Search and History Improvements

This commit is contained in:
Qstick
2020-10-21 18:53:04 -04:00
parent dd8313f9ce
commit 94b295ddcf
34 changed files with 148 additions and 472 deletions

View File

@@ -46,7 +46,7 @@ class IndexersSelectInputConnector extends Component {
IndexersSelectInputConnector.propTypes = { IndexersSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
indexerIds: PropTypes.number.isRequired, indexerIds: PropTypes.number,
value: PropTypes.arrayOf(PropTypes.number).isRequired, value: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired

View File

@@ -51,6 +51,12 @@
flex-grow: 1; flex-grow: 1;
} }
.actionMenu {
&:hover {
color: #515253;
}
}
.translate { .translate {
composes: link from '~Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
@@ -63,7 +69,7 @@
line-height: 60px; line-height: 60px;
&:hover { &:hover {
color: $toobarButtonHoverColor; color: #515253;
} }
} }

View File

@@ -158,8 +158,7 @@ MovieEditorFooter.propTypes = {
saveError: PropTypes.object, saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object, deleteError: PropTypes.object,
onSaveSelected: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired
onOrganizeMoviePress: PropTypes.func.isRequired
}; };
export default MovieEditorFooter; export default MovieEditorFooter;

View File

@@ -1,11 +0,0 @@
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
min-width: 70px;
}
.sourceTitle {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-word;
}

View File

@@ -1,162 +0,0 @@
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieLanguage from 'Indexer/MovieLanguage';
import MovieQuality from 'Indexer/MovieQuality';
import translate from 'Utilities/String/translate';
import styles from './MovieHistoryRow.css';
class MovieHistoryRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isMarkAsFailedModalOpen: false,
isDetailsModalOpen: false
};
}
//
// Listeners
onMarkAsFailedPress = () => {
this.setState({ isMarkAsFailedModalOpen: true });
}
onConfirmMarkAsFailed = () => {
this.props.onMarkAsFailedPress(this.props.id);
this.setState({ isMarkAsFailedModalOpen: false });
}
onMarkAsFailedModalClose = () => {
this.setState({ isMarkAsFailedModalOpen: false });
}
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
}
//
// Render
render() {
const {
eventType,
sourceTitle,
quality,
languages,
qualityCutoffNotMet,
date,
data,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
onMarkAsFailedPress
} = this.props;
const {
isMarkAsFailedModalOpen
} = this.state;
return (
<TableRow>
<HistoryEventTypeCell
eventType={eventType}
data={data}
/>
<TableRowCell className={styles.sourceTitle}>
{sourceTitle}
</TableRowCell>
<TableRowCell>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
<TableRowCell>
<MovieQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
<RelativeDateCellConnector
date={date}
/>
<TableRowCell className={styles.actions}>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
{
eventType === 'grabbed' &&
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
onPress={this.onMarkAsFailedPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedMessageText', [sourceTitle])}
confirmLabel={translate('MarkAsFailed')}
onConfirm={this.onConfirmMarkAsFailed}
onCancel={this.onMarkAsFailedModalClose}
/>
<HistoryDetailsModal
isOpen={this.state.isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onMarkAsFailedPress={onMarkAsFailedPress}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
MovieHistoryRow.propTypes = {
id: PropTypes.number.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
isMarkingAsFailed: PropTypes.bool,
movie: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default MovieHistoryRow;

View File

@@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import MovieHistoryRow from './MovieHistoryRow';
function createMapStateToProps() {
return createSelector(
createIndexerSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
return {
movie,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
const mapDispatchToProps = {
fetchHistory,
markAsFailed
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryRow);

View File

@@ -1,19 +0,0 @@
import React from 'react';
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
function MovieHistoryTable(props) {
const {
...otherProps
} = props;
return (
<MovieHistoryTableContentConnector
{...otherProps}
/>
);
}
MovieHistoryTable.propTypes = {
};
export default MovieHistoryTable;

View File

@@ -1,5 +0,0 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}

View File

@@ -1,114 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
import styles from './MovieHistoryTableContent.css';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'date',
label: translate('Date'),
isVisible: true
},
{
name: 'actions',
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
isVisible: true
}
];
class MovieHistoryTableContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress
} = this.props;
const hasItems = !!items.length;
return (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>Unable to load history</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>No history</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
MovieHistoryTableContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default MovieHistoryTableContent;

View File

@@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryTableContent from './MovieHistoryTableContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movieHistory,
(movieHistory) => {
return movieHistory;
}
);
}
const mapDispatchToProps = {
movieHistoryMarkAsFailed
};
class MovieHistoryTableContentConnector extends Component {
//
// Listeners
onMarkAsFailedPress = (historyId) => {
const {
movieId
} = this.props;
this.props.movieHistoryMarkAsFailed({
historyId,
movieId
});
}
//
// Render
render() {
return (
<MovieHistoryTableContent
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
);
}
}
MovieHistoryTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
movieHistoryMarkAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryTableContentConnector);

View File

@@ -15,11 +15,11 @@ function createMapStateToProps() {
createIndexerClientSideCollectionItemsSelector('indexerIndex'), createIndexerClientSideCollectionItemsSelector('indexerIndex'),
createDimensionsSelector(), createDimensionsSelector(),
( (
movies, indexers,
dimensionsState dimensionsState
) => { ) => {
return { return {
...movies, ...indexers,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }

View File

@@ -30,7 +30,7 @@ function SearchIndexSortMenu(props) {
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
name="sortTitle" name="title"
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}

View File

@@ -4,6 +4,7 @@ import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputCon
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import SearchFooterLabel from './SearchFooterLabel';
import styles from './SearchFooter.css'; import styles from './SearchFooter.css';
class SearchFooter extends Component { class SearchFooter extends Component {
@@ -61,9 +62,13 @@ class SearchFooter extends Component {
return ( return (
<PageContentFooter> <PageContentFooter>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<SearchFooterLabel
label={'Query'}
isSaving={false}
/>
<TextInput <TextInput
name='searchQuery' name='searchQuery'
placeholder='Query'
value={searchQuery} value={searchQuery}
isDisabled={isFetching} isDisabled={isFetching}
onChange={this.onInputChange} onChange={this.onInputChange}
@@ -71,6 +76,11 @@ class SearchFooter extends Component {
</div> </div>
<div className={styles.indexerContainer}> <div className={styles.indexerContainer}>
<SearchFooterLabel
label={'Indexers'}
isSaving={false}
/>
<IndexersSelectInputConnector <IndexersSelectInputConnector
name='indexerIds' name='indexerIds'
placeholder='Indexers' placeholder='Indexers'

View File

@@ -0,0 +1,8 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}

View File

@@ -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 './SearchFooterLabel.css';
function SearchFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
SearchFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
SearchFooterLabel.defaultProps = {
className: styles.label
};
export default SearchFooterLabel;

View File

@@ -11,6 +11,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props'; import { align, icons, sortDirections } from 'Helpers/Props';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
@@ -83,7 +84,7 @@ class SearchIndex extends Component {
} = this.props; } = this.props;
// Reset if not sorting by sortTitle // Reset if not sorting by sortTitle
if (sortKey !== 'sortTitle') { if (sortKey !== 'title') {
this.setState({ jumpBarItems: { order: [] } }); this.setState({ jumpBarItems: { order: [] } });
return; return;
} }
@@ -161,6 +162,7 @@ class SearchIndex extends Component {
onScroll, onScroll,
onSortSelect, onSortSelect,
onFilterSelect, onFilterSelect,
hasIndexers,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -174,8 +176,6 @@ class SearchIndex extends Component {
const isLoaded = !!(!error && isPopulated && items.length && scroller); const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems; const hasNoIndexer = !totalItems;
console.log(hasNoIndexer);
return ( return (
<PageContent> <PageContent>
<PageToolbar> <PageToolbar>
@@ -247,7 +247,12 @@ class SearchIndex extends Component {
} }
{ {
!error && !isFetching && !items.length && !error && !isFetching && !hasIndexers &&
<NoIndexer />
}
{
!error && !isFetching && hasIndexers && !items.length &&
<NoSearchResults totalItems={totalItems} /> <NoSearchResults totalItems={totalItems} />
} }
</PageContentBody> </PageContentBody>
@@ -286,7 +291,8 @@ SearchIndex.propTypes = {
onSortSelect: PropTypes.func.isRequired, onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired onScroll: PropTypes.func.isRequired,
hasIndexers: PropTypes.func.isRequired
}; };
export default SearchIndex; export default SearchIndex;

View File

@@ -6,19 +6,23 @@ import withScrollPosition from 'Components/withScrollPosition';
import { fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions'; import { fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector'; import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
import SearchIndex from './SearchIndex'; import SearchIndex from './SearchIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
createReleaseClientSideCollectionItemsSelector('releases'), createReleaseClientSideCollectionItemsSelector('releases'),
createDimensionsSelector(), createDimensionsSelector(),
( (
movies, indexers,
releases,
dimensionsState dimensionsState
) => { ) => {
return { return {
...movies, ...releases,
hasIndexers: indexers.items.length > 0,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }

View File

@@ -1,4 +1,4 @@
.status { .protocol {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 60px; flex: 0 0 60px;

View File

@@ -5,7 +5,7 @@
align-items: center; align-items: center;
} }
.status { .protocol {
composes: cell; composes: cell;
flex: 0 0 60px; flex: 0 0 60px;

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { setMovieSort } from 'Store/Actions/indexerIndexActions'; import { setReleasesSort } from 'Store/Actions/releaseActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import SearchIndexTable from './SearchIndexTable'; import SearchIndexTable from './SearchIndexTable';
@@ -23,7 +23,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onSortPress(sortKey) { onSortPress(sortKey) {
dispatch(setMovieSort({ sortKey })); dispatch(setReleasesSort({ sortKey }));
} }
}; };
} }

View File

@@ -27,6 +27,7 @@ class AddIndexerItem extends Component {
render() { render() {
const { const {
name,
implementation, implementation,
implementationName, implementationName,
infoLink, infoLink,
@@ -47,7 +48,7 @@ class AddIndexerItem extends Component {
<div className={styles.overlay}> <div className={styles.overlay}>
<div className={styles.name}> <div className={styles.name}>
{implementationName} {name}
</div> </div>
<div className={styles.actions}> <div className={styles.actions}>
@@ -101,6 +102,7 @@ class AddIndexerItem extends Component {
} }
AddIndexerItem.propTypes = { AddIndexerItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired, implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired, implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired, infoLink: PropTypes.string.isRequired,

View File

@@ -88,33 +88,6 @@ class UISettings extends Component {
id="uiSettings" id="uiSettings"
{...otherProps} {...otherProps}
> >
<FieldSet legend={translate('Calendar')}>
<FormGroup>
<FormLabel>{translate('SettingsFirstDayOfWeek')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="firstDayOfWeek"
values={firstDayOfWeekOptions}
onChange={onInputChange}
{...settings.firstDayOfWeek}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SettingsWeekColumnHeader')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="calendarWeekColumnHeader"
values={weekColumnOptions}
onChange={onInputChange}
helpText={translate('SettingsWeekColumnHeaderHelpText')}
{...settings.calendarWeekColumnHeader}
/>
</FormGroup>
</FieldSet>
<FieldSet legend={translate('Dates')}> <FieldSet legend={translate('Dates')}>
<FormGroup> <FormGroup>
<FormLabel>{translate('SettingsShortDateFormat')}</FormLabel> <FormLabel>{translate('SettingsShortDateFormat')}</FormLabel>

View File

@@ -60,25 +60,25 @@ export const defaultState = {
name: 'protocol', name: 'protocol',
label: translate('Protocol'), label: translate('Protocol'),
isSortable: true, isSortable: true,
isVisible: false isVisible: true
}, },
{ {
name: 'privacy', name: 'privacy',
label: translate('Privacy'), label: translate('Privacy'),
isSortable: true, isSortable: true,
isVisible: false isVisible: true
}, },
{ {
name: 'added', name: 'added',
label: translate('Added'), label: translate('Added'),
isSortable: true, isSortable: true,
isVisible: false isVisible: true
}, },
{ {
name: 'capabilities', name: 'capabilities',
label: 'Capabilities', label: 'Categories',
isSortable: false, isSortable: false,
isVisible: false isVisible: true
}, },
{ {
name: 'tags', name: 'tags',

View File

@@ -26,7 +26,7 @@ export const defaultState = {
isPopulated: false, isPopulated: false,
error: null, error: null,
items: [], items: [],
sortKey: 'releaseWeight', sortKey: 'title',
sortDirection: sortDirections.ASCENDING, sortDirection: sortDirections.ASCENDING,
columns: [ columns: [

View File

@@ -113,8 +113,8 @@ module.exports = {
// //
// Toolbar // Toolbar
toobarButtonHoverColor: '#ffc230', toobarButtonHoverColor: '#e66000',
toobarButtonSelectedColor: '#ffc230', toobarButtonSelectedColor: '#e66000',
// //
// Scroller // Scroller

View File

@@ -17,7 +17,7 @@ class MoreInfo extends Component {
<DescriptionList> <DescriptionList>
<DescriptionListItemTitle>Home page</DescriptionListItemTitle> <DescriptionListItemTitle>Home page</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to="https://prowlarr.video/">prowlarr.video</Link> <Link to="https://prowlarr.com/">prowlarr.com</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
<DescriptionListItemTitle>Discord</DescriptionListItemTitle> <DescriptionListItemTitle>Discord</DescriptionListItemTitle>

View File

@@ -13,11 +13,10 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public List<string> SceneTitles { get; set; } public List<string> SceneTitles { get; set; }
public virtual bool UserInvokedSearch { get; set; }
public virtual bool InteractiveSearch { get; set; } public virtual bool InteractiveSearch { get; set; }
public List<int> IndexerIds { get; set; }
public string ImdbId { get; set; } public string ImdbId { get; set; }
public int TmdbId { get; set; } public int TmdbId { get; set; }
public List<int> IndexerIds { get; set; }
public List<string> QueryTitles => SceneTitles.Select(GetQueryTitle).ToList(); public List<string> QueryTitles => SceneTitles.Select(GetQueryTitle).ToList();

View File

@@ -15,8 +15,8 @@ namespace NzbDrone.Core.IndexerSearch
{ {
public interface ISearchForNzb public interface ISearchForNzb
{ {
List<ReleaseInfo> Search(string query, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch); List<ReleaseInfo> Search(string query, List<int> indexerIds, bool interactiveSearch);
NewznabResults Search(NewznabRequest request, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch); NewznabResults Search(NewznabRequest request, List<int> indexerIds, bool interactiveSearch);
} }
public class NzbSearchService : ISearchForNzb public class NzbSearchService : ISearchForNzb
@@ -34,26 +34,25 @@ namespace NzbDrone.Core.IndexerSearch
_logger = logger; _logger = logger;
} }
public List<ReleaseInfo> Search(string query, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch) public List<ReleaseInfo> Search(string query, List<int> indexerIds, bool interactiveSearch)
{ {
var searchSpec = Get<MovieSearchCriteria>(query, indexerIds, userInvokedSearch, interactiveSearch); var searchSpec = Get<MovieSearchCriteria>(query, indexerIds, interactiveSearch);
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
} }
public NewznabResults Search(NewznabRequest request, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch) public NewznabResults Search(NewznabRequest request, List<int> indexerIds, bool interactiveSearch)
{ {
var searchSpec = Get<MovieSearchCriteria>(request.q, indexerIds, userInvokedSearch, interactiveSearch); var searchSpec = Get<MovieSearchCriteria>(request.q, indexerIds, interactiveSearch);
return new NewznabResults { Releases = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; return new NewznabResults { Releases = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
} }
private TSpec Get<TSpec>(string query, List<int> indexerIds, bool userInvokedSearch, bool interactiveSearch) private TSpec Get<TSpec>(string query, List<int> indexerIds, bool interactiveSearch)
where TSpec : SearchCriteriaBase, new() where TSpec : SearchCriteriaBase, new()
{ {
var spec = new TSpec() var spec = new TSpec()
{ {
UserInvokedSearch = userInvokedSearch,
InteractiveSearch = interactiveSearch InteractiveSearch = interactiveSearch
}; };

View File

@@ -53,10 +53,10 @@ namespace NzbDrone.Core.Indexers.Newznab
yield return GetDefinition("Nzb-Tortuga", GetSettings("https://www.nzb-tortuga.com")); yield return GetDefinition("Nzb-Tortuga", GetSettings("https://www.nzb-tortuga.com"));
yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su")); yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"));
yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat")); yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"));
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws", categories: new[] { 2030, 2040, 2045, 2050, 2060, 2070, 2080, 2090 })); yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws"));
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info")); yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net")); yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net"));
yield return GetDefinition("omgwtfnzbs", GetSettings("https://api.omgwtfnzbs.me", categories: new[] { 2000, 2020, 2030, 2040, 2045, 2050, 2070 })); yield return GetDefinition("omgwtfnzbs", GetSettings("https://api.omgwtfnzbs.me"));
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com")); yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com")); yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api")); yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));

View File

@@ -19,6 +19,27 @@ namespace NzbDrone.Core.Indexers.Rarbg
public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public; public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
};
public override TimeSpan RateLimit => TimeSpan.FromSeconds(2); public override TimeSpan RateLimit => TimeSpan.FromSeconds(2);
public Rarbg(IRarbgTokenProvider tokenProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) public Rarbg(IRarbgTokenProvider tokenProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)

View File

@@ -17,6 +17,7 @@
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="System.Text.Json" Version="4.7.2" /> <PackageReference Include="System.Text.Json" Version="4.7.2" />
<PackageReference Include="MonoTorrent" Version="1.0.19" /> <PackageReference Include="MonoTorrent" Version="1.0.19" />
<PackageReference Include="YamlDotNet" Version="8.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />

View File

@@ -20,7 +20,7 @@ namespace Prowlarr.Api.V1.History
GetResourcePaged = GetHistory; GetResourcePaged = GetHistory;
Get("/since", x => GetHistorySince()); Get("/since", x => GetHistorySince());
Get("/movie", x => GetMovieHistory()); Get("/indexer", x => GetIndexerHistory());
} }
protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeMovie) protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeMovie)
@@ -75,26 +75,26 @@ namespace Prowlarr.Api.V1.History
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeMovie)).ToList(); return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
} }
private List<HistoryResource> GetMovieHistory() private List<HistoryResource> GetIndexerHistory()
{ {
var queryMovieId = Request.Query.MovieId; var queryIndexerId = Request.Query.IndexerId;
var queryEventType = Request.Query.EventType; var queryEventType = Request.Query.EventType;
if (!queryMovieId.HasValue) if (!queryIndexerId.HasValue)
{ {
throw new BadRequestException("movieId is missing"); throw new BadRequestException("indexerId is missing");
} }
int movieId = Convert.ToInt32(queryMovieId.Value); int indexerId = Convert.ToInt32(queryIndexerId.Value);
HistoryEventType? eventType = null; HistoryEventType? eventType = null;
var includeMovie = Request.GetBooleanQueryParameter("includeMovie"); var includeIndexer = Request.GetBooleanQueryParameter("includeIndexer");
if (queryEventType.HasValue) if (queryEventType.HasValue)
{ {
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
} }
return _historyService.GetByIndexerId(movieId, eventType).Select(h => MapToResource(h, includeMovie)).ToList(); return _historyService.GetByIndexerId(indexerId, eventType).Select(h => MapToResource(h, includeIndexer)).ToList();
} }
} }
} }

View File

@@ -62,8 +62,10 @@ namespace Prowlarr.Api.V1.Indexers
Response response = indexerInstance.GetCapabilities().ToXml(); Response response = indexerInstance.GetCapabilities().ToXml();
response.ContentType = "application/rss+xml"; response.ContentType = "application/rss+xml";
return response; return response;
case "tvsearch":
case "music":
case "movie": case "movie":
Response movieResponse = _nzbSearchService.Search(request, new List<int> { indexer.Id }, true, false).ToXml(); Response movieResponse = _nzbSearchService.Search(request, new List<int> { indexer.Id }, false).ToXml();
movieResponse.ContentType = "application/rss+xml"; movieResponse.ContentType = "application/rss+xml";
return movieResponse; return movieResponse;
default: default:

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using Nancy.ModelBinding; using Nancy.ModelBinding;
using NLog; using NLog;
@@ -50,7 +49,7 @@ namespace Prowlarr.Api.V1.Search
{ {
try try
{ {
var decisions = _nzbSearhService.Search(query, indexerIds, true, true); var decisions = _nzbSearhService.Search(query, indexerIds, true);
return MapDecisions(decisions); return MapDecisions(decisions);
} }