Compare commits

...

25 Commits

Author SHA1 Message Date
Qstick
29e7cc06a1 Bump MonoTorrent to 2.0.7
Co-Authored-By: Winter <78392041+winterqt@users.noreply.github.com>
2022-12-24 15:16:00 -06:00
Qstick
387fb0bd15 Revert release analytics 2022-12-22 18:52:38 -06:00
Qstick
2d33560d89 Theme tweaks 2022-12-22 15:29:26 -06:00
Qstick
94a797fc1e New: (Nebulace) TVMaze Search Support
Fixes #1252
2022-12-22 14:32:03 -06:00
Qstick
2e851b0588 New: Mobile friendly manual search
Fixes #490
2022-12-22 13:37:09 -06:00
Qstick
7303cdf555 Fixed: Incorrect logic for newznab category parsing 2022-12-22 10:05:52 -06:00
Qstick
6636cbc4ae Fixed: (AnimeTosho) Mapping of Subcat as Parent 2022-12-22 08:47:45 -06:00
Weblate
a5a4f62f25 Translated using Weblate (Bengali)
Currently translated at 0.8% (4 of 468 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (468 of 468 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: saambd <me@salimrahman.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bn/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translation: Servarr/Prowlarr
2022-12-21 15:53:44 -06:00
bakerboy448
05a7465a07 Fixed: (MTV) Torznab Api Path
#1235
2022-12-21 15:01:51 -06:00
Qstick
c35f1212fb New: (Indexer) Torrent Bytes 2022-12-20 21:57:17 -06:00
Qstick
ad95d73e9d Fixed: Category parsing for some not-so-great Torznab feeds
MoreThanTv
2022-12-20 20:50:30 -06:00
Qstick
30f53c20ed New: Convert MoreThanTV to API
Fixes #1235
2022-12-20 17:59:50 -06:00
Servarr
0199a37a0c Automated API Docs update 2022-12-20 16:37:56 -06:00
Qstick
e9764820c0 Fixed: (SecretCinema) Title not being decoded
Fixes #1238
2022-12-20 16:24:34 -06:00
Qstick
d285cbb021 (SecretCinema) Avoid double HtmlDecode
#1238
2022-12-20 15:20:06 -06:00
Qstick
8afaa3386d Fixed: (PrivateHD) Remove Audio category
Fixes #1246
2022-12-20 15:12:07 -06:00
Qstick
c94beb6814 Fixed: Translations for Auth Settings 2022-12-20 14:58:58 -06:00
Qstick
c7eb08a0f0 New: Auth Required
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-12-20 14:23:09 -06:00
Qstick
2a2e859420 Update UI Dependencies 2022-12-20 14:23:09 -06:00
Qstick
31f0e8212e Update UI Dev Dependencies 2022-12-20 14:23:09 -06:00
Qstick
1cbb9b1724 Bump dotnet to 6.0.12 2022-12-20 14:23:09 -06:00
Qstick
45dbcc6b89 Bump version to 1.0.0 2022-12-20 14:23:09 -06:00
Qstick
3b26613394 Fixed: (Pornolab) Update Categories
Co-Authored-By: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2022-12-20 13:38:01 -06:00
Bakerboy448
6bb8c09fcf Log Skipped Application-Indexer Syncs at trace for support 2022-12-20 13:07:32 -06:00
Qstick
810b3612aa Fixed: Mapping of Year, Genre, other from search string 2022-12-20 12:34:56 -06:00
86 changed files with 3592 additions and 2703 deletions

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.11'
majorVersion: '1.0.0'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.301'
dotnetVersion: '6.0.404'
innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022'

View File

@@ -36,7 +36,6 @@ class TagInputInput extends Component {
<div
ref={forwardedRef}
className={className}
component="div"
onMouseDown={this.onMouseDown}
>
{

View File

@@ -36,5 +36,5 @@
/** Outline **/
.outline {
background-color: var(--white);
background-color: var(--cardBackgroundColor);
}

View File

@@ -108,5 +108,5 @@
/** Outline **/
.outline {
background-color: var(--white);
background-color: var(--cardBackgroundColor);
}

View File

@@ -5,7 +5,7 @@
text-align: center;
&:hover {
color: var(--toobarButtonHoverColor);
color: #515253;
}
}

View File

@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape';
import PageHeader from './Header/PageHeader';
import PageSidebar from './Sidebar/PageSidebar';
@@ -75,6 +76,7 @@ class Page extends Component {
isSmallScreen,
isSidebarVisible,
enableColorImpairedMode,
authenticationEnabled,
onSidebarToggle,
onSidebarVisibleChange
} = this.props;
@@ -109,6 +111,10 @@ class Page extends Component {
isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose}
/>
<AuthenticationRequiredModal
isOpen={!authenticationEnabled}
/>
</div>
</ColorImpairedContext.Provider>
);
@@ -124,6 +130,7 @@ Page.propTypes = {
isUpdated: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
authenticationEnabled: PropTypes.bool.isRequired,
onResize: PropTypes.func.isRequired,
onSidebarToggle: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired

View File

@@ -11,6 +11,7 @@ import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUI
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage';
import Page from './Page';
@@ -133,18 +134,21 @@ function createMapStateToProps() {
selectErrors,
selectAppProps,
createDimensionsSelector(),
createSystemStatusSelector(),
(
enableColorImpairedMode,
isPopulated,
errors,
app,
dimensions
dimensions,
systemStatus
) => {
return {
...app,
...errors,
isPopulated,
isSmallScreen: dimensions.isSmallScreen,
authenticationEnabled: systemStatus.authentication !== 'none',
enableColorImpairedMode
};
}

View File

@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModal(props) {
const {
isOpen
} = props;
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AuthenticationRequiredModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
AuthenticationRequiredModal.propTypes = {
isOpen: PropTypes.bool.isRequired
};
export default AuthenticationRequiredModal;

View File

@@ -0,0 +1,5 @@
.authRequiredAlert {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}

View File

@@ -0,0 +1,165 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModalContent(props) {
const {
isPopulated,
error,
isSaving,
settings,
onInputChange,
onSavePress,
dispatchFetchStatus
} = props;
const {
authenticationMethod,
authenticationRequired,
username,
password
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const didMount = useRef(false);
useEffect(() => {
if (!isSaving && didMount.current) {
dispatchFetchStatus();
}
didMount.current = true;
}, [isSaving, dispatchFetchStatus]);
return (
<ModalContent
showCloseButton={false}
onModalClose={onModalClose}
>
<ModalHeader>
{translate('AuthenticationRequired')}
</ModalHeader>
<ModalBody>
<Alert
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{authenticationRequiredWarning}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
{...username}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup> :
null
}
</div> :
null
}
{
!isPopulated && !error ? <LoadingIndicator /> : null
}
</ModalBody>
<ModalFooter>
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!authenticationEnabled}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
AuthenticationRequiredModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
settings: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default AuthenticationRequiredModalContent;

View File

@@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
const SECTION = 'general';
function createMapStateToProps() {
return createSelector(
createSettingsSectionSelector(SECTION),
(sectionSettings) => {
return {
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
dispatchClearPendingChanges: clearPendingChanges,
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
dispatchSaveGeneralSettings: saveGeneralSettings,
dispatchFetchGeneralSettings: fetchGeneralSettings,
dispatchFetchStatus: fetchStatus
};
class AuthenticationRequiredModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchGeneralSettings();
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetGeneralSettingsValue({ name, value });
};
onSavePress = () => {
this.props.dispatchSaveGeneralSettings();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchFetchGeneralSettings,
dispatchSetGeneralSettingsValue,
dispatchSaveGeneralSettings,
...otherProps
} = this.props;
return (
<AuthenticationRequiredModalContent
{...otherProps}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
/>
);
}
}
AuthenticationRequiredModalContentConnector.propTypes = {
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);

View File

@@ -226,6 +226,42 @@ class HistoryRow extends Component {
null
}
{
data.label ?
<HistoryRowParameter
title='Label'
value={data.label}
/> :
null
}
{
data.track ?
<HistoryRowParameter
title='Track'
value={data.track}
/> :
null
}
{
data.year ?
<HistoryRowParameter
title='Year'
value={data.year}
/> :
null
}
{
data.genre ?
<HistoryRowParameter
title='Genre'
value={data.genre}
/> :
null
}
{
data.author ?
<HistoryRowParameter
@@ -243,6 +279,15 @@ class HistoryRow extends Component {
/> :
null
}
{
data.publisher ?
<HistoryRowParameter
title='Publisher'
value={data.publisher}
/> :
null
}
</TableRowCell>
);
}

View File

@@ -0,0 +1,47 @@
$hoverScale: 1.05;
.content {
display: flex;
flex-grow: 0;
margin-left: 5px;
}
.container {
border-radius: 4px;
background-color: var(--cardBackgroundColor);
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.titleRow {
display: flex;
justify-content: space-between;
flex: 0 0 auto;
margin-bottom: 10px;
height: 38px;
}
.indexerRow {
color: var(--disabledColor);
}
.infoRow {
margin-bottom: 5px;
}
.title {
width: 85%;
font-weight: 500;
font-size: 14px;
}
.actions {
position: absolute;
right: 0;
white-space: nowrap;
}

View File

@@ -0,0 +1,189 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons, kinds } from 'Helpers/Props';
import CategoryLabel from 'Search/Table/CategoryLabel';
import Peers from 'Search/Table/Peers';
import ProtocolLabel from 'Search/Table/ProtocolLabel';
import dimensions from 'Styles/Variables/dimensions';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './SearchIndexOverview.css';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
function getContentHeight(rowHeight, isSmallScreen) {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadClient');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadClient');
}
class SearchIndexOverview extends Component {
//
// Listeners
onEditSeriesPress = () => {
this.setState({ isEditSeriesModalOpen: true });
};
onEditSeriesModalClose = () => {
this.setState({ isEditSeriesModalOpen: false });
};
//
// Render
render() {
const {
title,
protocol,
downloadUrl,
categories,
seeders,
leechers,
size,
age,
ageHours,
ageMinutes,
indexer,
rowHeight,
isSmallScreen,
isGrabbed,
isGrabbing,
grabError
} = this.props;
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.info} style={{ height: contentHeight }}>
<div className={styles.titleRow}>
<div className={styles.title}>
<TextTruncate
line={2}
text={title}
/>
</div>
<div className={styles.actions}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={this.onGrabPress}
/>
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl}
/>
</div>
</div>
<div className={styles.indexerRow}>
{indexer}
</div>
<div className={styles.infoRow}>
<ProtocolLabel
protocol={protocol}
/>
{
protocol === 'torrent' &&
<Peers
seeders={seeders}
leechers={leechers}
/>
}
<Label>
{formatBytes(size)}
</Label>
<Label>
{formatAge(age, ageHours, ageMinutes)}
</Label>
<CategoryLabel
categories={categories}
/>
</div>
</div>
</div>
</div>
);
}
}
SearchIndexOverview.propTypes = {
guid: PropTypes.string.isRequired,
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
ageHours: PropTypes.number.isRequired,
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string.isRequired,
indexerId: PropTypes.number.isRequired,
indexer: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
files: PropTypes.number,
grabs: PropTypes.number,
seeders: PropTypes.number,
leechers: PropTypes.number,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
rowHeight: PropTypes.number.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onGrabPress: PropTypes.func.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string
};
SearchIndexOverview.defaultProps = {
isGrabbing: false,
isGrabbed: false
};
export default SearchIndexOverview;

View File

@@ -0,0 +1,11 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: var(--tableRowHoverBackgroundColor);
}
}
}

View File

@@ -0,0 +1,211 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import SearchIndexItemConnector from 'Search/Table/SearchIndexItemConnector';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import SearchIndexOverview from './SearchIndexOverview';
import styles from './SearchIndexOverviews.css';
class SearchIndexOverviews extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnCount: 1,
rowHeight: 100,
scrollRestored: false
};
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items,
sortKey,
jumpToCharacter,
scrollTop,
isSmallScreen
} = this.props;
const {
width,
rowHeight,
scrollRestored
} = this.state;
if (prevProps.sortKey !== sortKey) {
this.calculateGrid(this.state.width, isSmallScreen);
}
if (
this._grid &&
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items)
)
) {
// 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 rowHeight = 100;
this.setState({
width,
rowHeight
});
};
cellRenderer = ({ key, rowIndex, style }) => {
const {
items,
sortKey,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
isSmallScreen,
onGrabPress
} = this.props;
const {
rowHeight
} = this.state;
const release = items[rowIndex];
return (
<div
className={styles.container}
key={key}
style={style}
>
<SearchIndexItemConnector
key={release.guid}
component={SearchIndexOverview}
sortKey={sortKey}
rowHeight={rowHeight}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
style={style}
guid={release.guid}
onGrabPress={onGrabPress}
/>
</div>
);
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
items
} = this.props;
const {
width,
rowHeight
} = this.state;
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
);
}
}
SearchIndexOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
scrollTop: PropTypes.number.isRequired,
jumpToCharacter: PropTypes.string,
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,
onGrabPress: PropTypes.func.isRequired
};
export default SearchIndexOverviews;

View File

@@ -0,0 +1,32 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { grabRelease } from 'Store/Actions/releaseActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import SearchIndexOverviews from './SearchIndexOverviews';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
createDimensionsSelector(),
(uiSettings, dimensions) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onGrabPress(payload) {
dispatch(grabRelease(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(SearchIndexOverviews);

View File

@@ -11,8 +11,6 @@ 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 AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -23,12 +21,17 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
import SearchIndexOverviewsConnector from './Mobile/SearchIndexOverviewsConnector';
import NoSearchResults from './NoSearchResults';
import SearchFooterConnector from './SearchFooterConnector';
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
import styles from './SearchIndex.css';
function getViewComponent() {
function getViewComponent(isSmallScreen) {
if (isSmallScreen) {
return SearchIndexOverviewsConnector;
}
return SearchIndexTableConnector;
}
@@ -44,8 +47,6 @@ class SearchIndex extends Component {
scroller: null,
jumpBarItems: { order: [] },
jumpToCharacter: null,
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false,
searchType: null,
lastToggled: null,
allSelected: false,
@@ -177,21 +178,6 @@ class SearchIndex extends Component {
//
// Listeners
onAddIndexerPress = () => {
this.setState({ isAddIndexerModalOpen: true });
};
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
this.setState({
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: indexerSelected
});
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
};
@@ -253,6 +239,7 @@ class SearchIndex extends Component {
onScroll,
onSortSelect,
onFilterSelect,
isSmallScreen,
hasIndexers,
...otherProps
} = this.props;
@@ -260,8 +247,6 @@ class SearchIndex extends Component {
const {
scroller,
jumpBarItems,
isAddIndexerModalOpen,
isEditIndexerModalOpen,
jumpToCharacter,
selectedState,
allSelected,
@@ -270,7 +255,7 @@ class SearchIndex extends Component {
const selectedIndexerIds = this.getSelectedIds();
const ViewComponent = getViewComponent();
const ViewComponent = getViewComponent(isSmallScreen);
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems;
@@ -384,16 +369,6 @@ class SearchIndex extends Component {
onSearchPress={this.onSearchPress}
onBulkGrabPress={this.onBulkGrabPress}
/>
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={this.onAddIndexerModalClose}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
/>
</PageContent>
);
}

View File

@@ -14,7 +14,7 @@
.category {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 110px;
flex: 0 0 130px;
}
.age,

View File

@@ -18,20 +18,20 @@ function createMapStateToProps() {
return createSelector(
createReleaseSelector(),
(
movie
release
) => {
// If a movie is deleted this selector may fire before the parent
// selecors, which will result in an undefined movie, if that happens
// If a release is deleted this selector may fire before the parent
// selecors, which will result in an undefined release, 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.
// trying to show a release that has no information available.
if (!movie) {
if (!release) {
return {};
}
return {
...movie
...release
};
}
);

View File

@@ -21,7 +21,7 @@
.category {
composes: cell;
flex: 0 0 110px;
flex: 0 0 130px;
}
.age,

View File

@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
export const authenticationMethodOptions = [
{ key: 'none', value: 'None', isDisabled: true },
{ key: 'external', value: 'External', isHidden: true },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
];
export const authenticationRequiredOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
];
const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
@@ -68,6 +76,7 @@ class SecuritySettings extends Component {
const {
authenticationMethod,
authenticationRequired,
username,
password,
apiKey,
@@ -86,13 +95,31 @@ class SecuritySettings extends Component {
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationRequiredWarning}
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
{
authenticationEnabled &&
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
@@ -102,11 +129,12 @@ class SecuritySettings extends Component {
onChange={onInputChange}
{...username}
/>
</FormGroup>
</FormGroup> :
null
}
{
authenticationEnabled &&
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
@@ -116,7 +144,8 @@ class SecuritySettings extends Component {
onChange={onInputChange}
{...password}
/>
</FormGroup>
</FormGroup> :
null
}
<FormGroup>

View File

@@ -1,4 +1,4 @@
import filesize from 'filesize';
import { filesize } from 'filesize';
function formatBytes(input) {
const size = Number(input);

View File

@@ -25,107 +25,108 @@
"not chrome < 60"
],
"dependencies": {
"@fortawesome/fontawesome-free": "6.1.1",
"@fortawesome/fontawesome-svg-core": "6.1.1",
"@fortawesome/free-regular-svg-icons": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.18",
"@microsoft/signalr": "6.0.6",
"@sentry/browser": "6.19.2",
"@sentry/integrations": "6.19.2",
"chart.js": "3.7.1",
"classnames": "2.3.1",
"clipboard": "2.0.10",
"connected-react-router": "6.9.1",
"@fortawesome/fontawesome-free": "6.2.1",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "0.2.0",
"@microsoft/signalr": "6.0.11",
"@sentry/browser": "7.28.0",
"@sentry/integrations": "7.28.0",
"chart.js": "4.1.1",
"classnames": "2.3.2",
"clipboard": "2.0.11",
"connected-react-router": "6.9.3",
"element-class": "0.2.2",
"filesize": "6.3.0",
"filesize": "10.0.6",
"history": "4.10.1",
"https-browserify": "1.0.0",
"jdu": "1.0.0",
"jquery": "3.6.0",
"jquery": "3.6.2",
"lodash": "4.17.21",
"mobile-detect": "1.4.5",
"moment": "2.29.2",
"moment": "2.29.4",
"mousetrap": "1.6.5",
"normalize.css": "8.0.1",
"prop-types": "15.8.1",
"qs": "6.10.3",
"qs": "6.11.0",
"react": "17.0.2",
"react-addons-shallow-compare": "15.6.3",
"react-async-script": "1.2.0",
"react-autosuggest": "10.1.0",
"react-custom-scrollbars-2": "4.4.0",
"react-custom-scrollbars-2": "4.5.0",
"react-dnd": "14.0.4",
"react-dnd-html5-backend": "14.0.2",
"react-dnd-multi-backend": "6.0.2",
"react-dnd-touch-backend": "14.1.1",
"react-document-title": "2.0.3",
"react-dom": "17.0.2",
"react-focus-lock": "2.5.0",
"react-focus-lock": "2.9.2",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.0",
"react-measure": "1.4.7",
"react-popper": "1.3.7",
"react-redux": "7.2.4",
"react-redux": "8.0.5",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-text-truncate": "0.19.0",
"react-virtualized": "9.21.1",
"redux": "4.1.0",
"redux": "4.2.0",
"redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0",
"redux-localstorage": "0.4.1",
"redux-thunk": "2.3.0",
"reselect": "4.0.0"
"redux-thunk": "2.4.2",
"reselect": "4.1.7"
},
"devDependencies": {
"@babel/core": "7.18.2",
"@babel/eslint-parser": "7.18.2",
"@babel/plugin-proposal-class-properties": "7.17.12",
"@babel/plugin-proposal-decorators": "7.18.2",
"@babel/plugin-proposal-export-default-from": "7.17.12",
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
"@babel/plugin-proposal-function-sent": "7.18.2",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
"@babel/plugin-proposal-numeric-separator": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.17.12",
"@babel/plugin-proposal-throw-expressions": "7.16.7",
"@babel/core": "7.20.5",
"@babel/eslint-parser": "7.19.1",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-decorators": "7.20.5",
"@babel/plugin-proposal-export-default-from": "7.18.10",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-function-sent": "7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-numeric-separator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.18.9",
"@babel/plugin-proposal-throw-expressions": "7.18.6",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.18.2",
"@babel/preset-react": "7.17.12",
"autoprefixer": "10.4.7",
"babel-loader": "8.2.5",
"@babel/preset-env": "7.20.2",
"@babel/preset-react": "7.18.6",
"autoprefixer": "10.4.13",
"babel-loader": "9.1.0",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.22.8",
"css-loader": "6.7.1",
"eslint": "8.17.0",
"core-js": "3.26.1",
"css-loader": "6.7.3",
"eslint": "8.30.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-react": "7.30.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-simple-import-sort": "8.0.0",
"esprint": "3.6.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "6.1.7",
"filemanager-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.0",
"loader-utils": "^3.0.0",
"mini-css-extract-plugin": "2.6.0",
"postcss": "8.4.14",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.2",
"postcss": "8.4.20",
"postcss-color-function": "4.1.0",
"postcss-loader": "6.2.1",
"postcss-mixins": "9.0.2",
"postcss-nested": "5.0.6",
"postcss-simple-vars": "6.0.3",
"postcss-loader": "7.0.2",
"postcss-mixins": "9.0.4",
"postcss-nested": "6.0.0",
"postcss-simple-vars": "7.0.1",
"postcss-url": "10.1.3",
"require-nocache": "1.0.0",
"rimraf": "3.0.2",
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.1",
"stylelint": "14.8.5",
"stylelint": "14.16.0",
"stylelint-order": "5.0.0",
"url-loader": "4.1.1",
"webpack": "5.73.0",
"webpack-cli": "4.9.2",
"webpack": "5.75.0",
"webpack-cli": "5.0.1",
"webpack-livereload-plugin": "3.0.2"
}
}

View File

@@ -94,7 +94,7 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />

View File

@@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner.KillAll();
_runner.Start();
_runner.Start(true);
driver.Url = "http://localhost:9696";

View File

@@ -11,26 +11,41 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
try
{
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
if (sentryEvent.Message is not null)
{
sentryEvent.Message.Formatted = CleanseLogMessage.Cleanse(sentryEvent.Message.Formatted);
sentryEvent.Message.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
sentryEvent.Message.Params = sentryEvent.Message.Params?.Select(x => CleanseLogMessage.Cleanse(x switch
{
string str => str,
_ => x.ToString()
})).ToList();
}
if (sentryEvent.Fingerprint != null)
if (sentryEvent.Fingerprint.Any())
{
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
sentryEvent.SetFingerprint(fingerprint);
}
if (sentryEvent.Extra != null)
if (sentryEvent.Extra.Any())
{
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
sentryEvent.SetExtras(extras);
}
foreach (var exception in sentryEvent.SentryExceptions)
if (sentryEvent.SentryExceptions is not null)
{
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
foreach (var frame in exception.Stacktrace.Frames)
foreach (var exception in sentryEvent.SentryExceptions)
{
frame.FileName = ShortenPath(frame.FileName);
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
if (exception.Stacktrace is not null)
{
foreach (var frame in exception.Stacktrace.Frames)
{
frame.FileName = ShortenPath(frame.FileName);
}
}
}
}
}

View File

@@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
"UnauthorizedAccessException",
// Filter out people stuck in boot loops
"CorruptDatabaseException",
// This also filters some people in boot loops
"TinyIoCResolutionException"
"CorruptDatabaseException"
};
public static readonly List<string> FilteredExceptionMessages = new List<string>
@@ -102,9 +99,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.Dsn = dsn;
o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200;
o.SendDefaultPii = false;
o.Debug = false;
o.DiagnosticLevel = SentryLevel.Debug;
o.Release = BuildInfo.Release;
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
@@ -210,7 +204,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
if (ex != null)
{
fingerPrint.Add(ex.GetType().FullName);
fingerPrint.Add(ex.TargetSite.ToString());
if (ex.TargetSite != null)
{
fingerPrint.Add(ex.TargetSite.ToString());
}
if (ex.InnerException != null)
{
fingerPrint.Add(ex.InnerException.GetType().FullName);

View File

@@ -4,18 +4,18 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.21.0" />
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="Sentry" Version="3.24.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />

View File

@@ -0,0 +1,144 @@
{
"status": "success",
"response": {
"currentPage": 1,
"pages": 1,
"results": [
{
"groupId": 2497,
"groupName": "Singin&#39; in the Rain",
"artist": "Gene Kelly & Stanley Donen",
"cover": "https:\/\/www.themoviedb.org\/t\/p\/original\/g2AaJDC2vSRcqHSDH29642xmQd.jpg",
"tags": [ "comedy", "musical", "romance" ],
"bookmarked": false,
"vanityHouse": false,
"groupYear": 1952,
"releaseType": null,
"groupTime": "1671129449",
"maxSize": 57473058680,
"totalSnatched": 25,
"totalSeeders": 9,
"totalLeechers": 0,
"torrents": [
{
"torrentId": 3599,
"editionId": 1,
"artists": [
{
"id": 126,
"name": "Gene Kelly",
"aliasid": 127
},
{
"id": 125,
"name": "Stanley Donen",
"aliasid": 126
}
],
"remastered": false,
"remasterYear": 0,
"remasterCatalogueNumber": "",
"remasterTitle": "",
"media": "1080p",
"encoding": "",
"format": "",
"hasLog": false,
"logScore": 0,
"hasCue": false,
"scene": false,
"vanityHouse": false,
"fileCount": 1,
"time": "2017-09-10 11:47:27",
"size": 24724893991,
"snatches": 14,
"seeders": 1,
"leechers": 0,
"isFreeleech": true,
"isNeutralLeech": false,
"isPersonalFreeleech": false,
"canUseToken": false,
"hasSnatched": false
},
{
"torrentId": 45068,
"editionId": 2,
"artists": [
{
"id": 126,
"name": "Gene Kelly",
"aliasid": 127
},
{
"id": 125,
"name": "Stanley Donen",
"aliasid": 126
}
],
"remastered": false,
"remasterYear": 0,
"remasterCatalogueNumber": "",
"remasterTitle": "",
"media": "2160p",
"encoding": "",
"format": "",
"hasLog": false,
"logScore": 0,
"hasCue": false,
"scene": false,
"vanityHouse": false,
"fileCount": 1,
"time": "2022-12-15 19:37:29",
"size": 57473058680,
"snatches": 6,
"seeders": 8,
"leechers": 0,
"isFreeleech": true,
"isNeutralLeech": false,
"isPersonalFreeleech": false,
"canUseToken": false,
"hasSnatched": false
},
{
"torrentId": 2726,
"editionId": 3,
"artists": [
{
"id": 126,
"name": "Gene Kelly",
"aliasid": 127
},
{
"id": 125,
"name": "Stanley Donen",
"aliasid": 126
}
],
"remastered": false,
"remasterYear": 0,
"remasterCatalogueNumber": "",
"remasterTitle": "",
"media": "DVD-R",
"encoding": "",
"format": "",
"hasLog": false,
"logScore": 0,
"hasCue": false,
"scene": false,
"vanityHouse": false,
"fileCount": 37,
"time": "2017-08-26 14:58:58",
"size": 10350032896,
"snatches": 5,
"seeders": 0,
"leechers": 0,
"isFreeleech": true,
"isNeutralLeech": false,
"isPersonalFreeleech": false,
"canUseToken": false,
"hasSnatched": false
}
]
}
]
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<caps>
<server version="1.0" title="Anime Tosho" strapline="Anime NZB/DDL mirror" url="https://animetosho.org/"/>
<limits max="200" default="75"/>
<retention days="9999"/>
<registration available="no" open="yes" />
<searching>
<search available="yes" supportedParams="q" />
<tv-search available="no" supportedParams="q" />
<movie-search available="no" supportedParams="q" />
</searching>
<categories>
<category id="5070" name="Anime" description="Anime"/>
</categories>
</caps>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:torznab="http://torznab.com/schemas/2015/feed">
<channel>
<item>
<title>Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv</title>
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836164</guid>
<link>https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)</link>
<comments>https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836164</comments>
<pubDate>Tue, 20 Dec 2022 21:32:17 +0000</pubDate>
<size>5412993028</size>
<files>1</files>
<grabs>2</grabs>
<category>2000</category>
<category>2040</category>
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
<enclosure url="https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)" length="103641" type="application/x-bittorrent" />
<torznab:attr name="size" value="5412993028" />
<torznab:attr name="poster" value="anon" />
<torznab:attr name="seeders" value="3" />
<torznab:attr name="leechers" value="0" />
<torznab:attr name="peers" value="3" />
<torznab:attr name="infohash" value="(removed)" />
<torznab:attr name="downloadvolumefactor" value="1" />
<torznab:attr name="uploadvolumefactor" value="1" />
<torznab:attr name="tag" value="anonymous" />
<torznab:attr name="imdb" value="0039689" />
<torznab:attr name="imdbid" value="tt0039689" />
</item>
<item>
<title>Out of the Past 1947 1080p USA Blu-ray AVC DTS-HD MA 2.0-PCH</title>
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836165</guid>
<link>https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)</link>
<comments>https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836165</comments>
<pubDate>Tue, 20 Dec 2022 21:47:40 +0000</pubDate>
<size>30524085127</size>
<files>78</files>
<grabs>0</grabs>
<category>2000</category>
<category>2040</category>
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
<enclosure url="https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)" length="150224" type="application/x-bittorrent" />
<torznab:attr name="size" value="30524085127" />
<torznab:attr name="poster" value="anon" />
<torznab:attr name="seeders" value="1" />
<torznab:attr name="leechers" value="0" />
<torznab:attr name="peers" value="1" />
<torznab:attr name="infohash" value="(removed)" />
<torznab:attr name="downloadvolumefactor" value="1" />
<torznab:attr name="uploadvolumefactor" value="1" />
<torznab:attr name="tag" value="anonymous" />
<torznab:attr name="imdb" value="0039689" />
<torznab:attr name="imdbid" value="tt0039689" />
</item>
</channel>
</rss>

View File

@@ -84,6 +84,19 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
bookCats.Should().Contain("8000");
}
[Test]
public void should_find_sub_categories_as_main_categories()
{
GivenCapsResponse(ReadAllText("Files/Indexers/Torznab/torznab_animetosho_caps.xml"));
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTrackerCatToNewznab("5070");
bookCats.Count.Should().Be(2);
bookCats.First().Id.Should().Be(5070);
}
[Test]
public void should_map_by_name_when_available()
{

View File

@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
}
[Test]
public async Task should_parse_recent_feed_from_GazelleGames()
public async Task should_parse_recent_feed_from_Orpheus()
{
var recentFeed = ReadAllText(@"Files/Indexers/Orpheus/recentfeed.json");

View File

@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
{
[TestFixture]
public class SecretCinemaFixture : CoreTest<SecretCinema>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "SecretCinema",
Settings = new GazelleSettings() { Username = "somekey", Password = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_SecretCinema()
{
var recentFeed = ReadAllText(@"Files/Indexers/SecretCinema/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(3);
releases.First().Should().BeOfType<GazelleInfo>();
var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("Singin' in the Rain (1952) 2160p");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://secret-cinema.pw/torrents.php?action=download&useToken=0&id=45068");
torrentInfo.InfoUrl.Should().Be("https://secret-cinema.pw/torrents.php?id=2497&torrentid=45068");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 19:37:29"));
torrentInfo.Size.Should().Be(57473058680);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(8);
torrentInfo.Seeders.Should().Be(8);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(0);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}

View File

@@ -34,6 +34,10 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
};
_caps = new IndexerCapabilities();
_caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies");
_caps.Categories.AddCategoryMapping(2040, NewznabStandardCategory.MoviesHD, "Movies/HD");
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
@@ -129,6 +133,38 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
releaseInfo.Peers.Should().BeNull();
}
[Test]
public async Task should_parse_recent_feed_from_torznab_morethantv()
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_morethantv.xml");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();
var releaseInfo = releases.First() as TorrentInfo;
releaseInfo.Title.Should().Be("Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
releaseInfo.DownloadUrl.Should().Be("https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)");
releaseInfo.InfoUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
releaseInfo.CommentUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
releaseInfo.PublishDate.Should().Be(DateTime.Parse("Tue, 20 Dec 2022 21:32:17 +0000").ToUniversalTime());
releaseInfo.Size.Should().Be(5412993028);
releaseInfo.TvdbId.Should().Be(0);
releaseInfo.TvRageId.Should().Be(0);
releaseInfo.InfoHash.Should().Be("(removed)");
releaseInfo.Seeders.Should().Be(3);
releaseInfo.Peers.Should().Be(3);
releaseInfo.Categories.Count().Should().Be(4);
}
[Test]
public void should_use_pagesize_reported_by_caps()
{

View File

@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="12.0.1" />
<PackageReference Include="YamlDotNet" Version="12.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -25,7 +25,6 @@ namespace NzbDrone.Core.Test.UpdateTests
}
[Test]
[Ignore("TODO No Updates On Server")]
public void finds_update_when_version_lower()
{
UseRealHttp();

View File

@@ -74,6 +74,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
}
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
}
public override void RemoveIndexer(int indexerId)

View File

@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Lidarr
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
}
public override void RemoveIndexer(int indexerId)

View File

@@ -75,6 +75,8 @@ namespace NzbDrone.Core.Applications.Mylar
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
}
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
}
public override void RemoveIndexer(int indexerId)

View File

@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Radarr
var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
}
public override void RemoveIndexer(int indexerId)

View File

@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Readarr
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
}
public override void RemoveIndexer(int indexerId)

View File

@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Sonarr
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
}
public override void RemoveIndexer(int indexerId)

View File

@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Whisparr
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
}
public override void RemoveIndexer(int indexerId)

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Authentication
{
public enum AuthenticationRequiredType
{
Enabled = 0,
DisabledForLocalAddresses = 1
}
}

View File

@@ -1,9 +1,10 @@
namespace NzbDrone.Core.Authentication
namespace NzbDrone.Core.Authentication
{
public enum AuthenticationType
{
None = 0,
Basic = 1,
Forms = 2
Forms = 2,
External = 3
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Authentication
@@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication
public Guid Identifier { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Salt { get; set; }
public int Iterations { get; set; }
}
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -21,6 +23,10 @@ namespace NzbDrone.Core.Authentication
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
private static readonly int ITERATIONS = 10000;
private static readonly int SALT_SIZE = 128 / 8;
private static readonly int NUMBER_OF_BYTES = 256 / 8;
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
{
_repo = repo;
@@ -30,12 +36,15 @@ namespace NzbDrone.Core.Authentication
public User Add(string username, string password)
{
return _repo.Insert(new User
var user = new User
{
Identifier = Guid.NewGuid(),
Username = username.ToLowerInvariant(),
Password = password.SHA256Hash()
});
Username = username.ToLowerInvariant()
};
SetUserHashedPassword(user, password);
return _repo.Insert(user);
}
public User Update(User user)
@@ -54,7 +63,7 @@ namespace NzbDrone.Core.Authentication
if (user.Password != password)
{
user.Password = password.SHA256Hash();
SetUserHashedPassword(user, password);
}
user.Username = username.ToLowerInvariant();
@@ -81,7 +90,20 @@ namespace NzbDrone.Core.Authentication
return null;
}
if (user.Password == password.SHA256Hash())
if (user.Salt.IsNullOrWhiteSpace())
{
// If password matches stored SHA256 hash, update to salted hash and verify.
if (user.Password == password.SHA256Hash())
{
SetUserHashedPassword(user, password);
return Update(user);
}
return null;
}
if (VerifyHashedPassword(user, password))
{
return user;
}
@@ -93,5 +115,42 @@ namespace NzbDrone.Core.Authentication
{
return _repo.FindUser(identifier);
}
private User SetUserHashedPassword(User user, string password)
{
var salt = GenerateSalt();
user.Iterations = ITERATIONS;
user.Salt = Convert.ToBase64String(salt);
user.Password = GetHashedPassword(password, salt, ITERATIONS);
return user;
}
private byte[] GenerateSalt()
{
var salt = new byte[SALT_SIZE];
RandomNumberGenerator.Create().GetBytes(salt);
return salt;
}
private string GetHashedPassword(string password, byte[] salt, int iterations)
{
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA512,
iterationCount: iterations,
numBytesRequested: NUMBER_OF_BYTES));
}
private bool VerifyHashedPassword(User user, string password)
{
var salt = Convert.FromBase64String(user.Salt);
var hashedPassword = GetHashedPassword(password, salt, user.Iterations);
return user.Password == hashedPassword;
}
}
}

View File

@@ -32,6 +32,7 @@ namespace NzbDrone.Core.Configuration
bool EnableSsl { get; }
bool LaunchBrowser { get; }
AuthenticationType AuthenticationMethod { get; }
AuthenticationRequiredType AuthenticationRequired { get; }
bool AnalyticsEnabled { get; }
string LogLevel { get; }
string ConsoleLogLevel { get; }
@@ -193,10 +194,12 @@ namespace NzbDrone.Core.Configuration
}
}
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
// TODO: Change back to "master" for the first stable release.
public string Branch => GetValue("Branch", "develop").ToLowerInvariant();
public string Branch => GetValue("Branch", "master").ToLowerInvariant();
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);

View File

@@ -0,0 +1,16 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(024)]
public class add_salt_to_users : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Users")
.AddColumn("Salt").AsString().Nullable()
.AddColumn("Iterations").AsInt32().Nullable();
}
}
}

View File

@@ -130,6 +130,8 @@ namespace NzbDrone.Core.History
history.Data.Add("ImdbId", ((MovieSearchCriteria)message.Query).FullImdbId ?? string.Empty);
history.Data.Add("TmdbId", ((MovieSearchCriteria)message.Query).TmdbId?.ToString() ?? string.Empty);
history.Data.Add("TraktId", ((MovieSearchCriteria)message.Query).TraktId?.ToString() ?? string.Empty);
history.Data.Add("Year", ((MovieSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
history.Data.Add("Genre", ((MovieSearchCriteria)message.Query).Genre ?? string.Empty);
}
if (message.Query is TvSearchCriteria)
@@ -142,18 +144,27 @@ namespace NzbDrone.Core.History
history.Data.Add("TvMazeId", ((TvSearchCriteria)message.Query).TvMazeId?.ToString() ?? string.Empty);
history.Data.Add("Season", ((TvSearchCriteria)message.Query).Season?.ToString() ?? string.Empty);
history.Data.Add("Episode", ((TvSearchCriteria)message.Query).Episode ?? string.Empty);
history.Data.Add("Year", ((TvSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
history.Data.Add("Genre", ((TvSearchCriteria)message.Query).Genre ?? string.Empty);
}
if (message.Query is MusicSearchCriteria)
{
history.Data.Add("Artist", ((MusicSearchCriteria)message.Query).Artist ?? string.Empty);
history.Data.Add("Album", ((MusicSearchCriteria)message.Query).Album ?? string.Empty);
history.Data.Add("Track", ((MusicSearchCriteria)message.Query).Track ?? string.Empty);
history.Data.Add("Label", ((MusicSearchCriteria)message.Query).Label ?? string.Empty);
history.Data.Add("Year", ((MusicSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
history.Data.Add("Genre", ((MusicSearchCriteria)message.Query).Genre ?? string.Empty);
}
if (message.Query is BookSearchCriteria)
{
history.Data.Add("Author", ((BookSearchCriteria)message.Query).Author ?? string.Empty);
history.Data.Add("BookTitle", ((BookSearchCriteria)message.Query).Title ?? string.Empty);
history.Data.Add("Publisher", ((BookSearchCriteria)message.Query).Publisher ?? string.Empty);
history.Data.Add("Year", ((BookSearchCriteria)message.Query).Year?.ToString() ?? string.Empty);
history.Data.Add("Genre", ((BookSearchCriteria)message.Query).Genre ?? string.Empty);
}
history.Data.Add("ElapsedTime", message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty);

View File

@@ -4,10 +4,10 @@ namespace NzbDrone.Core.IndexerSearch
{
public class NewznabRequest
{
private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:rid\:)(?<rid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:traktid\:)(?<traktid>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+)|(?:year\:)(?<year>[^{]+)|(?:genre\:)(?<genre>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public string t { get; set; }
public string q { get; set; }
@@ -61,6 +61,11 @@ namespace NzbDrone.Core.IndexerSearch
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["rid"].Success)
{
rid = int.TryParse(match.Groups["rid"].Value, out var rId) ? rId : null;
}
if (match.Groups["season"].Success)
{
season = int.TryParse(match.Groups["season"].Value, out var seasonParsed) ? seasonParsed : null;
@@ -71,11 +76,26 @@ namespace NzbDrone.Core.IndexerSearch
imdbid = match.Groups["imdbid"].Value;
}
if (match.Groups["traktid"].Success)
{
traktid = int.TryParse(match.Groups["traktid"].Value, out var trackId) ? trackId : null;
}
if (match.Groups["episode"].Success)
{
ep = match.Groups["episode"].Value;
}
if (match.Groups["year"].Success)
{
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
}
if (match.Groups["genre"].Success)
{
genre = match.Groups["genre"].Value;
}
q = q.Replace(match.Value, "");
}
}
@@ -93,7 +113,7 @@ namespace NzbDrone.Core.IndexerSearch
if (match.Groups["doubanid"].Success)
{
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var doubanId) ? doubanId : null;
}
if (match.Groups["imdbid"].Success)
@@ -101,6 +121,21 @@ namespace NzbDrone.Core.IndexerSearch
imdbid = match.Groups["imdbid"].Value;
}
if (match.Groups["traktid"].Success)
{
traktid = int.TryParse(match.Groups["traktid"].Value, out var trackId) ? trackId : null;
}
if (match.Groups["year"].Success)
{
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
}
if (match.Groups["genre"].Success)
{
genre = match.Groups["genre"].Value;
}
q = q.Replace(match.Value, "").Trim();
}
}
@@ -131,6 +166,16 @@ namespace NzbDrone.Core.IndexerSearch
label = match.Groups["label"].Value;
}
if (match.Groups["year"].Success)
{
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
}
if (match.Groups["genre"].Success)
{
genre = match.Groups["genre"].Value;
}
q = q.Replace(match.Value, "").Trim();
}
}
@@ -156,6 +201,16 @@ namespace NzbDrone.Core.IndexerSearch
publisher = match.Groups["publisher"].Value;
}
if (match.Groups["year"].Success)
{
year = int.TryParse(match.Groups["year"].Value, out var parsedYear) ? parsedYear : null;
}
if (match.Groups["genre"].Success)
{
genre = match.Groups["genre"].Value;
}
q = q.Replace(match.Value, "").Trim();
}
}

View File

@@ -1,85 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Analytics;
using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.IndexerSearch
{
public class ReleaseAnalyticsService : IHandleAsync<IndexerQueryEvent>
{
private readonly IHttpClient _httpClient;
private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IAnalyticsService _analyticsService;
private readonly Debouncer _debouncer;
private readonly Logger _logger;
private readonly List<ReleaseInfo> _pendingUpdates;
public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger)
{
_debouncer = new Debouncer(SendReleases, TimeSpan.FromMinutes(10));
_analyticsService = analyticsService;
_requestBuilder = requestBuilder.Releases;
_httpClient = httpClient;
_logger = logger;
_pendingUpdates = new List<ReleaseInfo>();
}
public void HandleAsync(IndexerQueryEvent message)
{
if (_analyticsService.IsEnabled && message.QueryResult?.Releases != null)
{
lock (_pendingUpdates)
{
_pendingUpdates.AddRange(message.QueryResult.Releases.Where(r => r.Title.IsNotNullOrWhiteSpace()));
}
_debouncer.Execute();
}
}
public void SendReleases()
{
lock (_pendingUpdates)
{
var pendingUpdates = _pendingUpdates.ToArray();
_pendingUpdates.Clear();
var request = _requestBuilder.Create().Resource("release/push").Build();
request.Method = HttpMethod.Post;
request.Headers.ContentType = "application/json";
request.SuppressHttpError = true;
request.LogHttpError = false;
var body = pendingUpdates.DistinctBy(r => r.Title).Select(x => new
{
Title = x.Title,
Categories = x.Categories?.Where(c => c.Id < 10000).Select(c => c.Id) ?? new List<int>(),
Protocol = x.DownloadProtocol.ToString(),
Size = x.Size,
PublishDate = x.PublishDate
});
try
{
request.SetContent(body.ToJson());
_httpClient.Post(request);
}
catch
{
_logger.Trace("Analytics push failed");
}
}
}
}
}

View File

@@ -9,20 +9,18 @@ using System.Web;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using FluentValidation;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions;
[Obsolete("Converted to Torznab")]
public class MoreThanTV : TorrentIndexerBase<CookieTorrentBaseSettings>
{
public override string Name => "MoreThanTV";

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Extensions;
@@ -49,7 +50,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvMazeId
}
};
@@ -107,16 +108,25 @@ namespace NzbDrone.Core.Indexers.Definitions
if (searchCriteria.SanitizedTvSearchString.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.SanitizedTvSearchString + "%";
queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedTvSearchString, @"[ -._]", "%").Trim() + "%";
}
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb))
if (searchCriteria.TvMazeId.HasValue)
{
queryParams.Tvmaze = searchCriteria.TvMazeId.Value;
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, @"[ -._]", "%").Trim() + "%";
}
}
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb))
{
queryParams.Imdb = intImdb;
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.EpisodeSearchString + "%";
queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, @"[ -._]", "%").Trim() + "%";
}
}
@@ -143,7 +153,7 @@ namespace NzbDrone.Core.Indexers.Definitions
if (searchCriteria.SanitizedSearchTerm.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.SanitizedSearchTerm + "%";
queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedSearchTerm, @"[ -._]", "%").Trim() + "%";
}
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));

View File

@@ -223,9 +223,16 @@ namespace NzbDrone.Core.Indexers.Newznab
foreach (var xmlCategory in xmlCategories.Elements("category"))
{
var parentName = xmlCategory.Attribute("name").Value;
var parentNameLower = parentName?.ToLowerInvariant();
var parentId = int.Parse(xmlCategory.Attribute("id").Value);
var mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => parentName.ToLower().Contains(x.Name.ToLower()));
var mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => parentNameLower.Contains(x.Name.ToLower()));
if (mappedCat == null)
{
// Try to find name and Id in AllCats for sub cats that are mapped as parents
mappedCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Id == parentId && x.Name.ToLower().Contains(parentNameLower));
}
if (mappedCat == null)
{

View File

@@ -127,6 +127,12 @@ namespace NzbDrone.Core.Indexers.Newznab
var cats = TryGetMultipleNewznabAttributes(item, "category");
var results = new List<IndexerCategory>();
// Try to find <category> elements for some indexers that suck at following the rules.
if (cats.Count == 0)
{
cats = item.Elements("category").Select(e => e.Value).ToList();
}
foreach (var cat in cats)
{
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);

View File

@@ -108,9 +108,9 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(1111, NewznabStandardCategory.XXXPack, "Паки полных фильмов / Full Length Movies Packs");
caps.Categories.AddCategoryMapping(508, NewznabStandardCategory.XXX, "Классические фильмы / Classic");
caps.Categories.AddCategoryMapping(555, NewznabStandardCategory.XXX, "Фильмы с сюжетом / Feature & Vignettes");
caps.Categories.AddCategoryMapping(1673, NewznabStandardCategory.XXX, "Гонзо-фильмы 2011-2021 / Gonzo 2011-2021");
caps.Categories.AddCategoryMapping(1673, NewznabStandardCategory.XXX, "Гонзо-фильмы 2011-2023 / Gonzo 2011-2023");
caps.Categories.AddCategoryMapping(1112, NewznabStandardCategory.XXX, "Фильмы без сюжета 1991-2010 / All Sex & Amateur 1991-2010");
caps.Categories.AddCategoryMapping(1718, NewznabStandardCategory.XXX, "Фильмы без сюжета 2011-2021 / All Sex & Amateur 2011-2021");
caps.Categories.AddCategoryMapping(1718, NewznabStandardCategory.XXX, "Фильмы без сюжета 2011-2023 / All Sex & Amateur 2011-2023");
caps.Categories.AddCategoryMapping(553, NewznabStandardCategory.XXX, "Лесбо-фильмы / All Girl & Solo");
caps.Categories.AddCategoryMapping(1143, NewznabStandardCategory.XXX, "Этнические фильмы / Ethnic-Themed");
caps.Categories.AddCategoryMapping(1646, NewznabStandardCategory.XXX, "Видео для телефонов и КПК / Pocket РС & Phone Video");
@@ -125,8 +125,8 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(1675, NewznabStandardCategory.XXXPack, "Паки русских порнороликов / Russian Clips Packs");
caps.Categories.AddCategoryMapping(36, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 1991-2015 / Russian SiteRip's 1991-2015");
caps.Categories.AddCategoryMapping(1830, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 1991-2015 (HD Video) / Russian SiteRip's 1991-2015 (HD Video)");
caps.Categories.AddCategoryMapping(1803, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 2016-2021 / Russian SiteRip's 2016-2021");
caps.Categories.AddCategoryMapping(1831, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 2016-2021 (HD Video) / Russian SiteRip's 2016-2021 (HD Video)");
caps.Categories.AddCategoryMapping(1803, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 2016-2023 / Russian SiteRip's 2016-2023");
caps.Categories.AddCategoryMapping(1831, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 2016-2023 (HD Video) / Russian SiteRip's 2016-2023 (HD Video)");
caps.Categories.AddCategoryMapping(1741, NewznabStandardCategory.XXX, "Русские Порноролики Разное / Russian Clips (various)");
caps.Categories.AddCategoryMapping(1676, NewznabStandardCategory.XXX, "Русское любительское видео / Russian Amateur Video");
@@ -145,6 +145,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(1842, NewznabStandardCategory.XXX, "Сайтрипы 2020 (HD Video) / SiteRip's 2020 (HD Video)");
caps.Categories.AddCategoryMapping(1846, NewznabStandardCategory.XXX, "Сайтрипы 2021 (HD Video) / SiteRip's 2021 (HD Video)");
caps.Categories.AddCategoryMapping(1857, NewznabStandardCategory.XXX, "Сайтрипы 2022 (HD Video) / SiteRip's 2022 (HD Video)");
caps.Categories.AddCategoryMapping(1861, NewznabStandardCategory.XXX, "Сайтрипы 2023 (HD Video) / SiteRip's 2023 (HD Video)");
caps.Categories.AddCategoryMapping(1451, NewznabStandardCategory.XXX, "Сайтрипы 1991-2010 / SiteRip's 1991-2010");
caps.Categories.AddCategoryMapping(1788, NewznabStandardCategory.XXX, "Сайтрипы 2011-2012 / SiteRip's 2011-2012");
@@ -158,6 +159,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(1843, NewznabStandardCategory.XXX, "Сайтрипы 2020 / SiteRip's 2020");
caps.Categories.AddCategoryMapping(1847, NewznabStandardCategory.XXX, "Сайтрипы 2021 / SiteRip's 2021");
caps.Categories.AddCategoryMapping(1856, NewznabStandardCategory.XXX, "Сайтрипы 2022 / SiteRip's 2022");
caps.Categories.AddCategoryMapping(1862, NewznabStandardCategory.XXX, "Сайтрипы 2023 / SiteRip's 2023");
caps.Categories.AddCategoryMapping(1707, NewznabStandardCategory.XXX, "Сцены из фильмов / Movie Scenes");
caps.Categories.AddCategoryMapping(284, NewznabStandardCategory.XXX, "Порноролики Разное / Clips (various)");
@@ -166,7 +168,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(1801, NewznabStandardCategory.XXXPack, "Паки японских фильмов и сайтрипов / Full Length Japanese Movies Packs & SiteRip's Packs");
caps.Categories.AddCategoryMapping(1719, NewznabStandardCategory.XXX, "Японские фильмы и сайтрипы (DVD и HD Video) / Japanese Movies & SiteRip's (DVD & HD Video)");
caps.Categories.AddCategoryMapping(997, NewznabStandardCategory.XXX, "Японские фильмы и сайтрипы 1991-2014 / Japanese Movies & SiteRip's 1991-2014");
caps.Categories.AddCategoryMapping(1818, NewznabStandardCategory.XXX, "Японские фильмы и сайтрипы 2015-2019 / Japanese Movies & SiteRip's 2015-2019");
caps.Categories.AddCategoryMapping(1818, NewznabStandardCategory.XXX, "Японские фильмы и сайтрипы 2015-2023 / Japanese Movies & SiteRip's 2015-2023");
caps.Categories.AddCategoryMapping(1671, NewznabStandardCategory.XXX, "Эротические студии (видео) / Erotic Video Library");
caps.Categories.AddCategoryMapping(1726, NewznabStandardCategory.XXX, "Met-Art & MetModels");
@@ -205,6 +207,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(1734, NewznabStandardCategory.XXX, "Фистинг и дилдо / Fisting & Dildo");
caps.Categories.AddCategoryMapping(1791, NewznabStandardCategory.XXX, "Беременные / Pregnant");
caps.Categories.AddCategoryMapping(509, NewznabStandardCategory.XXX, "Буккаке / Bukkake");
caps.Categories.AddCategoryMapping(1859, NewznabStandardCategory.XXX, "Гэнг-бэнг / GangBang");
caps.Categories.AddCategoryMapping(1685, NewznabStandardCategory.XXX, "Мочеиспускание / Peeing");
caps.Categories.AddCategoryMapping(1762, NewznabStandardCategory.XXX, "Фетиш / Fetish");

View File

@@ -40,10 +40,6 @@ namespace NzbDrone.Core.Indexers.Definitions
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.Genre
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
}
};
@@ -55,7 +51,6 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVUHD);
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVHD);
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVSD);
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.Audio);
return caps;
}

View File

@@ -2,24 +2,16 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FluentValidation;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
@@ -113,13 +105,12 @@ namespace NzbDrone.Core.Indexers.Definitions
// in SC movies, artist=director and GroupName=title
var artist = WebUtility.HtmlDecode(result.Artist);
var album = WebUtility.HtmlDecode(result.GroupName);
var title = WebUtility.HtmlDecode(result.GroupName);
var release = new GazelleInfo()
{
Guid = string.Format("SecretCinema-{0}", id),
Title = WebUtility.HtmlDecode(title),
Title = title,
Container = torrent.Encoding,
Files = torrent.FileCount,
Grabs = torrent.Snatches,
@@ -131,6 +122,8 @@ namespace NzbDrone.Core.Indexers.Definitions
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
PublishDate = torrent.Time.ToUniversalTime(),
Scene = torrent.Scene,
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
};
var category = torrent.Category;
@@ -147,7 +140,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
// Remove director from title
// SC API returns no more useful information than this
release.Title = $"{result.GroupName} ({result.GroupYear}) {torrent.Media}";
release.Title = $"{title} ({result.GroupYear}) {torrent.Media}";
// Replace media formats with standards
release.Title = Regex.Replace(release.Title, "BDMV", "COMPLETE BLURAY", RegexOptions.IgnoreCase);
@@ -156,7 +149,7 @@ namespace NzbDrone.Core.Indexers.Definitions
else
{
// SC API currently doesn't return anything but title.
release.Title = $"{result.Artist} - {result.GroupName} ({result.GroupYear}) [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
release.Title = $"{artist} - {title} ({result.GroupYear}) [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
}
if (torrent.HasCue)
@@ -184,6 +177,8 @@ namespace NzbDrone.Core.Indexers.Definitions
Files = result.FileCount,
Grabs = result.Snatches,
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
};
var category = result.Category;

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
{
public class TorrentBytes : TorrentIndexerBase<UserPassTorrentBaseSettings>
{
public override string Name => "Torrent Bytes";
public override string[] IndexerUrls => new string[] { "https://www.torrentbytes.net/" };
private string LoginUrl => Settings.BaseUrl + "takelogin.php";
public override string Description => "A decade of TorrentBytes";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.GetEncoding("iso-8859-1");
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public TorrentBytes(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new TorrentBytesRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new TorrentBytesParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true
};
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var cookies = Cookies;
Cookies = null;
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("returnto", "/")
.AddFormParameter("login", "Log in!")
.SetHeader("Content-Type", "multipart/form-data")
.Build();
var headers = new NameValueCollection
{
{ "Referer", LoginUrl }
};
authLoginRequest.Headers.Add(headers);
var response = await ExecuteAuth(authLoginRequest);
if (CheckIfLoginNeeded(response))
{
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var errorMessage = dom.QuerySelector("td.embedded").TextContent.Trim();
throw new IndexerAuthException(errorMessage);
}
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("TorrentBytes authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("my.php"))
{
return true;
}
return false;
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(23, NewznabStandardCategory.TVAnime, "Anime");
caps.Categories.AddCategoryMapping(52, NewznabStandardCategory.PCMac, "Apple/All");
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.PC, "Apps/misc");
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.PC, "Apps/PC");
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.TVForeign, "Foreign Titles");
caps.Categories.AddCategoryMapping(50, NewznabStandardCategory.Console, "Games/Consoles");
caps.Categories.AddCategoryMapping(42, NewznabStandardCategory.PCGames, "Games/Pack");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.PCGames, "Games/PC");
caps.Categories.AddCategoryMapping(51, NewznabStandardCategory.PC, "Linux/All");
caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.OtherMisc, "Misc");
caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.MoviesDVD, "Movies/DVD-R");
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.MoviesBluRay, "Movies/Full Blu-ray");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.MoviesHD, "Movies/HD");
caps.Categories.AddCategoryMapping(40, NewznabStandardCategory.Movies, "Movies/Pack");
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.MoviesSD, "Movies/SD");
caps.Categories.AddCategoryMapping(49, NewznabStandardCategory.MoviesUHD, "Movies/UHD");
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.Audio, "Music/DVDR");
caps.Categories.AddCategoryMapping(48, NewznabStandardCategory.AudioLossless, "Music/Flac");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.AudioMP3, "Music/MP3");
caps.Categories.AddCategoryMapping(43, NewznabStandardCategory.Audio, "Music/Pack");
caps.Categories.AddCategoryMapping(34, NewznabStandardCategory.AudioVideo, "Music/Videos");
caps.Categories.AddCategoryMapping(45, NewznabStandardCategory.MoviesBluRay, "NonScene/BRrip");
caps.Categories.AddCategoryMapping(46, NewznabStandardCategory.MoviesHD, "NonScene/x264");
caps.Categories.AddCategoryMapping(44, NewznabStandardCategory.MoviesSD, "NonScene/Xvid");
caps.Categories.AddCategoryMapping(37, NewznabStandardCategory.TVHD, "TV/BRrip");
caps.Categories.AddCategoryMapping(38, NewznabStandardCategory.TVHD, "TV/HD");
caps.Categories.AddCategoryMapping(41, NewznabStandardCategory.TV, "TV/Pack");
caps.Categories.AddCategoryMapping(33, NewznabStandardCategory.TVSD, "TV/SD");
caps.Categories.AddCategoryMapping(32, NewznabStandardCategory.TVUHD, "TV/UHD");
caps.Categories.AddCategoryMapping(39, NewznabStandardCategory.XXXx264, "XXX/HD");
caps.Categories.AddCategoryMapping(24, NewznabStandardCategory.XXXImageSet, "XXX/IMGSET");
caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.XXXPack, "XXX/Pack");
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.XXXXviD, "XXX/SD");
caps.Categories.AddCategoryMapping(29, NewznabStandardCategory.XXX, "XXX/Web");
return caps;
}
}
public class TorrentBytesRequestGenerator : IIndexerRequestGenerator
{
public UserPassTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public TorrentBytesRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
{
var searchUrl = string.Format("{0}/browse.php", Settings.BaseUrl.TrimEnd('/'));
var qc = new NameValueCollection
{
{ "incldead", "1" }
};
if (imdbId.IsNotNullOrWhiteSpace())
{
qc.Add("search", imdbId);
qc.Add("sc", "2"); // search in description
}
else
{
qc.Add("search", term);
qc.Add("sc", "1"); // search in title
}
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories))
{
qc.Add("c" + cat, "1");
}
searchUrl = searchUrl + "?" + qc.GetQueryString();
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class TorrentBytesParser : IParseIndexerResponse
{
private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public TorrentBytesParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var doc = parser.ParseDocument(indexerResponse.Content);
var rows = doc.QuerySelectorAll("table > tbody:has(tr > td.colhead) > tr:not(:has(td.colhead))");
foreach (var row in rows)
{
var release = new TorrentInfo();
var link = row.QuerySelector("td:nth-of-type(2) a:nth-of-type(2)");
release.Guid = _settings.BaseUrl + link.GetAttribute("href");
release.InfoUrl = release.Guid;
release.Title = link.GetAttribute("title");
// There isn't a title attribute if the release name isn't truncated.
if (string.IsNullOrWhiteSpace(release.Title))
{
release.Title = link.FirstChild.TextContent.Trim();
}
release.Description = release.Title;
// If we search an get no results, we still get a table just with no info.
if (string.IsNullOrWhiteSpace(release.Title))
{
break;
}
// Check if the release has been assigned a category
var qCat = row.QuerySelector("td:nth-of-type(1) a");
if (qCat != null)
{
var cat = qCat.GetAttribute("href").Substring(15);
release.Categories = _categories.MapTrackerCatToNewznab(cat);
}
var qLink = row.QuerySelector("td:nth-of-type(2) a");
release.DownloadUrl = _settings.BaseUrl + qLink.GetAttribute("href");
var added = row.QuerySelector("td:nth-of-type(5)").TextContent.Trim();
release.PublishDate = DateTime.ParseExact(added, "yyyy-MM-ddHH:mm:ss", CultureInfo.InvariantCulture);
var sizeStr = row.QuerySelector("td:nth-of-type(7)").TextContent.Trim();
release.Size = ParseUtil.GetBytes(sizeStr);
release.Seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(9)").TextContent.Trim());
release.Peers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(10)").TextContent.Trim()) +
release.Seeders;
var files = row.QuerySelector("td:nth-child(3)").TextContent;
release.Files = ParseUtil.CoerceInt(files);
var grabs = row.QuerySelector("td:nth-child(8)").TextContent;
if (grabs != "----")
{
release.Grabs = ParseUtil.CoerceInt(grabs);
}
release.DownloadVolumeFactor = row.QuerySelector("font[color=\"green\"]:contains(\"F\"):contains(\"L\")") != null ? 0 : 1;
release.UploadVolumeFactor = 1;
torrentInfos.Add(release);
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
}

View File

@@ -88,8 +88,9 @@ namespace NzbDrone.Core.Indexers.Torznab
{
get
{
yield return GetDefinition("AnimeTosho", GetSettings("https://feed.animetosho.org"));
yield return GetDefinition("Generic Torznab", GetSettings(""));
yield return GetDefinition("AnimeTosho", "", GetSettings("https://feed.animetosho.org"));
yield return GetDefinition("MoreThanTV", "Private torrent tracker for TV / MOVIES", GetSettings("https://www.morethantv.me", apiPath: @"/api/torznab"));
yield return GetDefinition("Generic Torznab", "A Newznab-like api for torrents.", GetSettings(""));
}
}
@@ -99,12 +100,13 @@ namespace NzbDrone.Core.Indexers.Torznab
_capabilitiesProvider = capabilitiesProvider;
}
private IndexerDefinition GetDefinition(string name, TorznabSettings settings)
private IndexerDefinition GetDefinition(string name, string description, TorznabSettings settings)
{
return new IndexerDefinition
{
Enable = true,
Name = name,
Description = description,
Implementation = GetType().Name,
Settings = settings,
Protocol = DownloadProtocol.Usenet,

View File

@@ -165,9 +165,15 @@ namespace NzbDrone.Core.Indexers.Torznab
protected override ICollection<IndexerCategory> GetCategory(XElement item)
{
var capabilities = _capabilitiesProvider.GetCapabilities(_settings, _definition);
var cats = TryGetMultipleNewznabAttributes(item, "category");
var cats = TryGetMultipleTorznabAttributes(item, "category");
var results = new List<IndexerCategory>();
// Try to find <category> elements for some indexers that suck at following the rules.
if (cats.Count == 0)
{
cats = item.Elements("category").Select(e => e.Value).ToList();
}
foreach (var cat in cats)
{
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);
@@ -265,7 +271,7 @@ namespace NzbDrone.Core.Indexers.Torznab
return defaultValue;
}
protected List<string> TryGetMultipleNewznabAttributes(XElement item, string key)
protected List<string> TryGetMultipleTorznabAttributes(XElement item, string key)
{
var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
var results = new List<string>();

View File

@@ -1 +1,6 @@
{}
{
"About": "সম্পর্কিত",
"Actions": "ক্রিয়াকাণ্ড",
"Add": "যোগ করুন",
"AcceptConfirmationModal": "নিশ্চিতকরণ মডেল গ্রহণ করুন"
}

View File

@@ -45,6 +45,9 @@
"Auth": "Auth",
"Authentication": "Authentication",
"AuthenticationMethodHelpText": "Require Username and Password to access Prowlarr",
"AuthenticationRequired": "Authentication Required",
"AuthenticationRequiredHelpText": "Change which requests authentication is required for. Do not change unless you understand the risks.",
"AuthenticationRequiredWarning": "To prevent remote access without authentication, Prowlarr now requires authentication to be enabled. You can optionally disable authentication from local addresses.",
"Automatic": "Automatic",
"AutomaticSearch": "Automatic Search",
"Backup": "Backup",

View File

@@ -462,5 +462,9 @@
"Queued": "Jonossa",
"Started": "Alkoi",
"ApplicationLongTermStatusCheckAllClientMessage": "Sovellukset eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi.",
"ApplicationLongTermStatusCheckSingleClientMessage": "Sovellukset eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {0}"
"ApplicationLongTermStatusCheckSingleClientMessage": "Sovellukset eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {0}",
"AreYouSureYouWantToDeleteCategory": "Haluatko varmasti poistaa kartoitetun kategorian?",
"DeleteClientCategory": "Poista lataustyökalukategoria",
"DownloadClientCategory": "Lataustyökalukategoria",
"MappedCategories": "Kartoitetut kategoriat"
}

View File

@@ -462,5 +462,9 @@
"Started": "Elkezdődött",
"NextExecution": "Következő végrehajtás",
"ApplicationLongTermStatusCheckSingleClientMessage": "Az alkamazások elérhetetlenek több mint 6 órája az alábbi hiba miatt: {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "Az összes alkalmazás elérhetetlen több mint 6 órája meghibásodás miatt"
"ApplicationLongTermStatusCheckAllClientMessage": "Az összes alkalmazás elérhetetlen több mint 6 órája meghibásodás miatt",
"AreYouSureYouWantToDeleteCategory": "Biztosan törölni szeretnéd a leképezett kategóriát?",
"DeleteClientCategory": "Letöltési kliens kategória törlése",
"MappedCategories": "Térképezett kategóriák",
"DownloadClientCategory": "Letöltési kliens kategória"
}

View File

@@ -6,7 +6,8 @@
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
<PackageReference Include="MailKit" Version="3.4.1" />
<PackageReference Include="MailKit" Version="3.4.3" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.12" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
@@ -15,13 +16,13 @@
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="TinyTwitter" Version="1.1.2" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="MonoTorrent" Version="2.0.5" />
<PackageReference Include="YamlDotNet" Version="12.0.1" />
<PackageReference Include="MonoTorrent" Version="2.0.7" />
<PackageReference Include="YamlDotNet" Version="12.3.1" />
<PackageReference Include="AngleSharp" Version="0.17.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,11 +4,11 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -22,7 +22,6 @@ using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host.AccessControl;
using NzbDrone.Http.Authentication;
using NzbDrone.SignalR;
using Prowlarr.Api.V1.System;
using Prowlarr.Http;
@@ -172,6 +171,8 @@ namespace NzbDrone.Host
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"]));
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, UiAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("SignalR", policy =>

View File

@@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -37,12 +37,12 @@ namespace NzbDrone.Test.Common
Port = port;
}
public void Start()
public void Start(bool enableAuth = false)
{
AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + TestBase.GetUID());
Directory.CreateDirectory(AppData);
GenerateConfigFile();
GenerateConfigFile(enableAuth);
string consoleExe;
if (OsInfo.IsWindows)
@@ -167,7 +167,7 @@ namespace NzbDrone.Test.Common
}
}
private void GenerateConfigFile()
private void GenerateConfigFile(bool enableAuth)
{
var configFile = Path.Combine(AppData, "config.xml");
@@ -180,6 +180,8 @@ namespace NzbDrone.Test.Common
new XElement(nameof(ConfigFileProvider.ApiKey), apiKey),
new XElement(nameof(ConfigFileProvider.LogLevel), "trace"),
new XElement(nameof(ConfigFileProvider.AnalyticsEnabled), false),
new XElement(nameof(ConfigFileProvider.AuthenticationMethod), enableAuth ? "Forms" : "None"),
new XElement(nameof(ConfigFileProvider.AuthenticationRequired), "DisabledForLocalAddresses"),
new XElement(nameof(ConfigFileProvider.Port), Port)));
var data = xDoc.ToString();

View File

@@ -6,7 +6,7 @@
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />

View File

@@ -4,9 +4,9 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />

View File

@@ -4,7 +4,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -15,6 +15,7 @@ namespace Prowlarr.Api.V1.Config
public bool EnableSsl { get; set; }
public bool LaunchBrowser { get; set; }
public AuthenticationType AuthenticationMethod { get; set; }
public AuthenticationRequiredType AuthenticationRequired { get; set; }
public bool AnalyticsEnabled { get; set; }
public string Username { get; set; }
public string Password { get; set; }
@@ -57,6 +58,7 @@ namespace Prowlarr.Api.V1.Config
EnableSsl = model.EnableSsl,
LaunchBrowser = model.LaunchBrowser,
AuthenticationMethod = model.AuthenticationMethod,
AuthenticationRequired = model.AuthenticationRequired,
AnalyticsEnabled = model.AnalyticsEnabled,
//Username

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />

View File

@@ -4548,11 +4548,19 @@
],
"type": "string"
},
"AuthenticationRequiredType": {
"enum": [
"enabled",
"disabledForLocalAddresses"
],
"type": "string"
},
"AuthenticationType": {
"enum": [
"none",
"basic",
"forms"
"forms",
"external"
],
"type": "string"
},
@@ -5145,6 +5153,9 @@
"authenticationMethod": {
"$ref": "#/components/schemas/AuthenticationType"
},
"authenticationRequired": {
"$ref": "#/components/schemas/AuthenticationRequiredType"
},
"analyticsEnabled": {
"type": "boolean"
},

View File

@@ -13,6 +13,7 @@ namespace Prowlarr.Http.Authentication
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "API Key";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;

View File

@@ -22,10 +22,16 @@ namespace Prowlarr.Http.Authentication
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
}
public static AuthenticationBuilder AddExternal(this AuthenticationBuilder authenticationBuilder, string name)
{
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
}
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
{
return services.AddAuthentication()
.AddNone(AuthenticationType.None.ToString())
.AddExternal(AuthenticationType.External.ToString())
.AddBasic(AuthenticationType.Basic.ToString())
.AddCookie(AuthenticationType.Forms.ToString(), options =>
{

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Prowlarr.Http.Authentication
{
public class BypassableDenyAnonymousAuthorizationRequirement : DenyAnonymousAuthorizationRequirement
{
}
}

View File

@@ -0,0 +1,45 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Messaging.Events;
using Prowlarr.Http.Extensions;
namespace Prowlarr.Http.Authentication
{
public class UiAuthorizationHandler : AuthorizationHandler<BypassableDenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement, IHandle<ConfigSavedEvent>
{
private readonly IConfigFileProvider _configService;
private static AuthenticationRequiredType _authenticationRequired;
public UiAuthorizationHandler(IConfigFileProvider configService)
{
_configService = configService;
_authenticationRequired = configService.AuthenticationRequired;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BypassableDenyAnonymousAuthorizationRequirement requirement)
{
if (_authenticationRequired == AuthenticationRequiredType.DisabledForLocalAddresses)
{
if (context.Resource is HttpContext httpContext &&
IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress) &&
ipAddress.IsLocalAddress())
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
public void Handle(ConfigSavedEvent message)
{
_authenticationRequired = _configService.AuthenticationRequired;
}
}
}

View File

@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Http.Authentication
namespace Prowlarr.Http.Authentication
{
public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
@@ -29,7 +29,8 @@ namespace NzbDrone.Http.Authentication
if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase))
{
var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString())
.RequireAuthenticatedUser();
.AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement());
return Task.FromResult(policy.Build());
}

View File

@@ -5,7 +5,7 @@
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="ImpromptuInterface" Version="7.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />

3986
yarn.lock

File diff suppressed because it is too large Load Diff