mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
31 Commits
v1.24.3.47
...
v1.25.2.47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c05be39346 | ||
|
|
951d42a591 | ||
|
|
dd046d8a68 | ||
|
|
efa54a4d51 | ||
|
|
3f07c50cc5 | ||
|
|
94cf07ddb4 | ||
|
|
24063e06ab | ||
|
|
e8ebb87189 | ||
|
|
896e196767 | ||
|
|
9f5be75e6d | ||
|
|
9cc9e720bb | ||
|
|
a9c2cca66d | ||
|
|
9cc3646be5 | ||
|
|
d6bca449da | ||
|
|
cb5764c654 | ||
|
|
19a9b56fa4 | ||
|
|
a2b0f199f1 | ||
|
|
59bfad7614 | ||
|
|
aee3f2d12b | ||
|
|
11d58b4460 | ||
|
|
ee4de6c6ca | ||
|
|
8d16b88185 | ||
|
|
121ef8e80d | ||
|
|
d53fec7e75 | ||
|
|
c017a3cd7e | ||
|
|
27ea93090f | ||
|
|
d79845144e | ||
|
|
3f77900dd0 | ||
|
|
4e8b9e81cf | ||
|
|
a32ab3acfd | ||
|
|
942da3a5c0 |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.24.3'
|
||||
majorVersion: '1.25.2'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
@@ -20,7 +20,7 @@ variables:
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-12'
|
||||
macImage: 'macOS-13'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './FormInputButton.css';
|
||||
|
||||
function FormInputButton(props) {
|
||||
const {
|
||||
className,
|
||||
canSpin,
|
||||
isLastButton,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
if (canSpin) {
|
||||
return (
|
||||
<SpinnerButton
|
||||
className={classNames(
|
||||
className,
|
||||
!isLastButton && styles.middleButton
|
||||
)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={classNames(
|
||||
className,
|
||||
!isLastButton && styles.middleButton
|
||||
)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
FormInputButton.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
isLastButton: PropTypes.bool.isRequired,
|
||||
canSpin: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
FormInputButton.defaultProps = {
|
||||
className: styles.button,
|
||||
isLastButton: true,
|
||||
canSpin: false
|
||||
};
|
||||
|
||||
export default FormInputButton;
|
||||
38
frontend/src/Components/Form/FormInputButton.tsx
Normal file
38
frontend/src/Components/Form/FormInputButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Button, { ButtonProps } from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './FormInputButton.css';
|
||||
|
||||
export interface FormInputButtonProps extends ButtonProps {
|
||||
canSpin?: boolean;
|
||||
isLastButton?: boolean;
|
||||
}
|
||||
|
||||
function FormInputButton({
|
||||
className = styles.button,
|
||||
canSpin = false,
|
||||
isLastButton = true,
|
||||
...otherProps
|
||||
}: FormInputButtonProps) {
|
||||
if (canSpin) {
|
||||
return (
|
||||
<SpinnerButton
|
||||
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormInputButton;
|
||||
@@ -1,48 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './Label.css';
|
||||
|
||||
function Label(props) {
|
||||
const {
|
||||
className,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
children,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
Label.defaultProps = {
|
||||
className: styles.label,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.SMALL,
|
||||
outline: false
|
||||
};
|
||||
|
||||
export default Label;
|
||||
33
frontend/src/Components/Label.tsx
Normal file
33
frontend/src/Components/Label.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import styles from './Label.css';
|
||||
|
||||
export interface LabelProps extends ComponentProps<'span'> {
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
outline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Label({
|
||||
className = styles.label,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.SMALL,
|
||||
outline = false,
|
||||
...otherProps
|
||||
}: LabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import Link from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
class Button extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
buttonGroupPosition,
|
||||
kind,
|
||||
size,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
buttonGroupPosition: PropTypes.oneOf(align.all),
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
size: PropTypes.oneOf(sizes.all),
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
className: styles.button,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.MEDIUM
|
||||
};
|
||||
|
||||
export default Button;
|
||||
37
frontend/src/Components/Link/Button.tsx
Normal file
37
frontend/src/Components/Link/Button.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import Link, { LinkProps } from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
||||
buttonGroupPosition?: Extract<
|
||||
(typeof align.all)[number],
|
||||
keyof typeof styles
|
||||
>;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
children: Required<LinkProps['children']>;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
className = styles.button,
|
||||
buttonGroupPosition,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.MEDIUM,
|
||||
...otherProps
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import Clipboard from 'clipboard';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import styles from './ClipboardButton.css';
|
||||
|
||||
class ClipboardButton extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._id = getUniqueElememtId();
|
||||
this._successTimeout = null;
|
||||
this._testResultTimeout = null;
|
||||
|
||||
this.state = {
|
||||
showSuccess: false,
|
||||
showError: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._clipboard = new Clipboard(`#${this._id}`, {
|
||||
text: () => this.props.value,
|
||||
container: document.getElementById(this._id)
|
||||
});
|
||||
|
||||
this._clipboard.on('success', this.onSuccess);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
showSuccess,
|
||||
showError
|
||||
} = this.state;
|
||||
|
||||
if (showSuccess || showError) {
|
||||
this._testResultTimeout = setTimeout(this.resetState, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._clipboard) {
|
||||
this._clipboard.destroy();
|
||||
}
|
||||
|
||||
if (this._testResultTimeout) {
|
||||
clearTimeout(this._testResultTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = () => {
|
||||
this.setState({
|
||||
showSuccess: false,
|
||||
showError: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSuccess = () => {
|
||||
this.setState({
|
||||
showSuccess: true
|
||||
});
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
this.setState({
|
||||
showError: true
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
className,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
showSuccess,
|
||||
showError
|
||||
} = this.state;
|
||||
|
||||
const showStateIcon = showSuccess || showError;
|
||||
const iconName = showError ? icons.DANGER : icons.CHECK;
|
||||
const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
|
||||
|
||||
return (
|
||||
<FormInputButton
|
||||
id={this._id}
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
<span className={showStateIcon ? styles.showStateIcon : undefined}>
|
||||
{
|
||||
showSuccess &&
|
||||
<span className={styles.stateIconContainer}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
<span className={styles.clipboardIconContainer}>
|
||||
<Icon name={icons.CLIPBOARD} />
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</FormInputButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardButton.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
ClipboardButton.defaultProps = {
|
||||
className: styles.button
|
||||
};
|
||||
|
||||
export default ClipboardButton;
|
||||
76
frontend/src/Components/Link/ClipboardButton.tsx
Normal file
76
frontend/src/Components/Link/ClipboardButton.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import copy from 'copy-to-clipboard';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { ButtonProps } from './Button';
|
||||
import styles from './ClipboardButton.css';
|
||||
|
||||
export interface ClipboardButtonProps extends Omit<ButtonProps, 'children'> {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ClipboardState = 'success' | 'error' | null;
|
||||
|
||||
export default function ClipboardButton({
|
||||
id,
|
||||
value,
|
||||
className = styles.button,
|
||||
...otherProps
|
||||
}: ClipboardButtonProps) {
|
||||
const [state, setState] = useState<ClipboardState>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
setState(null);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
try {
|
||||
if ('clipboard' in navigator) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
} else {
|
||||
copy(value);
|
||||
}
|
||||
|
||||
setState('success');
|
||||
} catch (e) {
|
||||
setState('error');
|
||||
console.error(`Failed to copy to clipboard`, e);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<FormInputButton
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
{...otherProps}
|
||||
>
|
||||
<span className={state ? styles.showStateIcon : undefined}>
|
||||
{state ? (
|
||||
<span className={styles.stateIconContainer}>
|
||||
<Icon
|
||||
name={state === 'error' ? icons.DANGER : icons.CHECK}
|
||||
kind={state === 'error' ? kinds.DANGER : kinds.SUCCESS}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<span className={styles.clipboardIconContainer}>
|
||||
<Icon name={icons.CLIPBOARD} />
|
||||
</span>
|
||||
</span>
|
||||
</FormInputButton>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +1,93 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ComponentClass,
|
||||
FunctionComponent,
|
||||
ComponentPropsWithoutRef,
|
||||
ElementType,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
interface ReactRouterLinkProps {
|
||||
to?: string;
|
||||
}
|
||||
export type LinkProps<C extends ElementType = 'button'> =
|
||||
ComponentPropsWithoutRef<C> & {
|
||||
component?: C;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: LinkProps<C>['disabled'];
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
};
|
||||
|
||||
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
||||
className?: string;
|
||||
component?:
|
||||
| string
|
||||
| FunctionComponent<LinkProps>
|
||||
| ComponentClass<LinkProps, unknown>;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: boolean;
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
}
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
className,
|
||||
component = 'button',
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter = false,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
export default function Link<C extends ElementType = 'button'>({
|
||||
className,
|
||||
component,
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: LinkProps<C>) {
|
||||
const Component = component || 'button';
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onPress?.(event);
|
||||
},
|
||||
[isDisabled, onPress]
|
||||
);
|
||||
|
||||
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
||||
target,
|
||||
};
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if (/\w+?:\/\//.test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
const linkClass = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const elementProps = {
|
||||
...otherProps,
|
||||
type,
|
||||
...linkProps,
|
||||
};
|
||||
if (to) {
|
||||
const toLink = /\w+?:\/\//.test(to);
|
||||
|
||||
elementProps.onClick = onClick;
|
||||
if (toLink || noRouter) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
target={target || (toLink ? '_blank' : '_self')}
|
||||
rel={toLink ? 'noreferrer' : undefined}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement(el, elementProps);
|
||||
return (
|
||||
<RouterLink
|
||||
to={`${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
type={
|
||||
component === 'button' || component === 'input'
|
||||
? type || 'button'
|
||||
: type
|
||||
}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Link;
|
||||
|
||||
@@ -7,7 +7,6 @@ export const PRIMARY = 'primary';
|
||||
export const PURPLE = 'purple';
|
||||
export const SUCCESS = 'success';
|
||||
export const WARNING = 'warning';
|
||||
export const QUEUE = 'queue';
|
||||
|
||||
export const all = [
|
||||
DANGER,
|
||||
@@ -19,5 +18,15 @@ export const all = [
|
||||
PURPLE,
|
||||
SUCCESS,
|
||||
WARNING,
|
||||
QUEUE
|
||||
];
|
||||
] as const;
|
||||
|
||||
export type Kind =
|
||||
| 'danger'
|
||||
| 'default'
|
||||
| 'disabled'
|
||||
| 'info'
|
||||
| 'inverse'
|
||||
| 'primary'
|
||||
| 'purple'
|
||||
| 'success'
|
||||
| 'warning';
|
||||
@@ -4,4 +4,6 @@ export const MEDIUM = 'medium';
|
||||
export const LARGE = 'large';
|
||||
export const EXTRA_LARGE = 'extraLarge';
|
||||
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const;
|
||||
|
||||
export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge';
|
||||
@@ -19,6 +19,7 @@ interface SavePayload {
|
||||
seedRatio?: number;
|
||||
seedTime?: number;
|
||||
packSeedTime?: number;
|
||||
preferMagnetUrl?: boolean;
|
||||
}
|
||||
|
||||
interface EditIndexerModalContentProps {
|
||||
@@ -65,6 +66,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
|
||||
null
|
||||
);
|
||||
const [preferMagnetUrl, setPreferMagnetUrl] = useState<
|
||||
null | string | boolean
|
||||
>(null);
|
||||
|
||||
const save = useCallback(() => {
|
||||
let hasChanges = false;
|
||||
@@ -105,6 +109,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
payload.packSeedTime = packSeedTime as number;
|
||||
}
|
||||
|
||||
if (preferMagnetUrl !== null) {
|
||||
hasChanges = true;
|
||||
payload.preferMagnetUrl = preferMagnetUrl === 'true';
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
onSavePress(payload);
|
||||
}
|
||||
@@ -118,6 +127,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
seedRatio,
|
||||
seedTime,
|
||||
packSeedTime,
|
||||
preferMagnetUrl,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
]);
|
||||
@@ -146,6 +156,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
case 'packSeedTime':
|
||||
setPackSeedTime(value);
|
||||
break;
|
||||
case 'preferMagnetUrl':
|
||||
setPreferMagnetUrl(value);
|
||||
break;
|
||||
default:
|
||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||
}
|
||||
@@ -254,6 +267,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('PreferMagnetUrl')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="preferMagnetUrl"
|
||||
value={preferMagnetUrl}
|
||||
values={enableOptions}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
.minimumSeeders,
|
||||
.seedRatio,
|
||||
.seedTime,
|
||||
.packSeedTime {
|
||||
.packSeedTime,
|
||||
.preferMagnetUrl {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 90px;
|
||||
|
||||
@@ -11,6 +11,7 @@ interface CssExports {
|
||||
'id': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'preferMagnetUrl': string;
|
||||
'priority': string;
|
||||
'privacy': string;
|
||||
'protocol': string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
@@ -74,6 +75,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
|
||||
?.value ?? undefined;
|
||||
|
||||
const preferMagnetUrl =
|
||||
fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')
|
||||
?.value ?? undefined;
|
||||
|
||||
const rssUrl = `${window.location.origin}${
|
||||
window.Prowlarr.urlBase
|
||||
}/${id}/api?apikey=${encodeURIComponent(
|
||||
@@ -102,6 +107,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
setIsDeleteIndexerModalOpen(false);
|
||||
}, [setIsDeleteIndexerModalOpen]);
|
||||
|
||||
const checkInputCallback = useCallback(() => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
}, []);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
||||
selectDispatch({
|
||||
@@ -277,6 +286,21 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'preferMagnetUrl') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{preferMagnetUrl === undefined ? null : (
|
||||
<CheckInput
|
||||
name="preferMagnetUrl"
|
||||
value={preferMagnetUrl}
|
||||
isDisabled={true}
|
||||
onChange={checkInputCallback}
|
||||
/>
|
||||
)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
.minimumSeeders,
|
||||
.seedRatio,
|
||||
.seedTime,
|
||||
.packSeedTime {
|
||||
.packSeedTime,
|
||||
.preferMagnetUrl {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 90px;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface CssExports {
|
||||
'id': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'preferMagnetUrl': string;
|
||||
'priority': string;
|
||||
'privacy': string;
|
||||
'protocol': string;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { createSelector } from 'reselect';
|
||||
import Alert from 'Components/Alert';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
@@ -26,23 +24,12 @@ import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||
import useIndexer from 'Indexer/useIndexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerHistory from './History/IndexerHistory';
|
||||
import styles from './IndexerInfoModalContent.css';
|
||||
|
||||
function createIndexerInfoItemSelector(indexerId: number) {
|
||||
return createSelector(
|
||||
createIndexerSelectorForHook(indexerId),
|
||||
(indexer?: Indexer) => {
|
||||
return {
|
||||
indexer,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = ['details', 'categories', 'history', 'stats'];
|
||||
const TABS = ['details', 'categories', 'history', 'stats'];
|
||||
|
||||
interface IndexerInfoModalContentProps {
|
||||
indexerId: number;
|
||||
@@ -51,9 +38,7 @@ interface IndexerInfoModalContentProps {
|
||||
}
|
||||
|
||||
function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
const { indexerId, onCloneIndexerPress } = props;
|
||||
|
||||
const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId));
|
||||
const { indexerId, onModalClose, onCloneIndexerPress } = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
@@ -67,53 +52,53 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
protocol,
|
||||
privacy,
|
||||
capabilities = {} as IndexerCapabilities,
|
||||
} = indexer as Indexer;
|
||||
} = useIndexer(indexerId) as Indexer;
|
||||
|
||||
const { onModalClose } = props;
|
||||
|
||||
const baseUrl =
|
||||
fields.find((field) => field.name === 'baseUrl')?.value ??
|
||||
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
|
||||
|
||||
const vipExpiration =
|
||||
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(tabs[0]);
|
||||
const [selectedTab, setSelectedTab] = useState(TABS[0]);
|
||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const onTabSelect = useCallback(
|
||||
(index: number) => {
|
||||
const selectedTab = tabs[index];
|
||||
const handleTabSelect = useCallback(
|
||||
(selectedIndex: number) => {
|
||||
const selectedTab = TABS[selectedIndex];
|
||||
setSelectedTab(selectedTab);
|
||||
},
|
||||
[setSelectedTab]
|
||||
);
|
||||
|
||||
const onEditIndexerPress = useCallback(() => {
|
||||
const handleEditIndexerPress = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(true);
|
||||
}, [setIsEditIndexerModalOpen]);
|
||||
|
||||
const onEditIndexerModalClose = useCallback(() => {
|
||||
const handleEditIndexerModalClose = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
}, [setIsEditIndexerModalOpen]);
|
||||
|
||||
const onDeleteIndexerPress = useCallback(() => {
|
||||
const handleDeleteIndexerPress = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
setIsDeleteIndexerModalOpen(true);
|
||||
}, [setIsDeleteIndexerModalOpen]);
|
||||
|
||||
const onDeleteIndexerModalClose = useCallback(() => {
|
||||
const handleDeleteIndexerModalClose = useCallback(() => {
|
||||
setIsDeleteIndexerModalOpen(false);
|
||||
onModalClose();
|
||||
}, [setIsDeleteIndexerModalOpen, onModalClose]);
|
||||
|
||||
const onCloneIndexerPressWrapper = useCallback(() => {
|
||||
const handleCloneIndexerPressWrapper = useCallback(() => {
|
||||
onCloneIndexerPress(id);
|
||||
onModalClose();
|
||||
}, [id, onCloneIndexerPress, onModalClose]);
|
||||
|
||||
const baseUrl =
|
||||
fields.find((field) => field.name === 'baseUrl')?.value ??
|
||||
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
|
||||
|
||||
const indexerUrl = baseUrl?.replace(/(:\/\/)api\./, '$1');
|
||||
|
||||
const vipExpiration =
|
||||
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{`${name}`}</ModalHeader>
|
||||
@@ -121,8 +106,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
<ModalBody>
|
||||
<Tabs
|
||||
className={styles.tabs}
|
||||
selectedIndex={tabs.indexOf(selectedTab)}
|
||||
onSelect={onTabSelect}
|
||||
selectedIndex={TABS.indexOf(selectedTab)}
|
||||
onSelect={handleTabSelect}
|
||||
>
|
||||
<TabList className={styles.tabList}>
|
||||
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||
@@ -178,10 +163,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
{translate('IndexerSite')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{baseUrl ? (
|
||||
<Link to={baseUrl}>
|
||||
{baseUrl.replace(/(:\/\/)api\./, '$1')}
|
||||
</Link>
|
||||
{indexerUrl ? (
|
||||
<Link to={indexerUrl}>{indexerUrl}</Link>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
@@ -365,16 +348,16 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteIndexerPress}
|
||||
onPress={handleDeleteIndexerPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
<Button onPress={onCloneIndexerPressWrapper}>
|
||||
<Button onPress={handleCloneIndexerPressWrapper}>
|
||||
{translate('Clone')}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button onPress={onEditIndexerPress}>{translate('Edit')}</Button>
|
||||
<Button onPress={handleEditIndexerPress}>{translate('Edit')}</Button>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
@@ -382,14 +365,14 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
<EditIndexerModalConnector
|
||||
isOpen={isEditIndexerModalOpen}
|
||||
id={id}
|
||||
onModalClose={onEditIndexerModalClose}
|
||||
onDeleteIndexerPress={onDeleteIndexerPress}
|
||||
onModalClose={handleEditIndexerModalClose}
|
||||
onDeleteIndexerPress={handleDeleteIndexerPress}
|
||||
/>
|
||||
|
||||
<DeleteIndexerModal
|
||||
isOpen={isDeleteIndexerModalOpen}
|
||||
indexerId={id}
|
||||
onModalClose={onDeleteIndexerModalClose}
|
||||
onModalClose={handleDeleteIndexerModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
|
||||
19
frontend/src/Indexer/useIndexer.ts
Normal file
19
frontend/src/Indexer/useIndexer.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export function createIndexerSelector(indexerId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.indexers.itemMap,
|
||||
(state: AppState) => state.indexers.items,
|
||||
(itemMap, allIndexers) => {
|
||||
return indexerId ? allIndexers[itemMap[indexerId]] : undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function useIndexer(indexerId?: number) {
|
||||
return useSelector(createIndexerSelector(indexerId));
|
||||
}
|
||||
|
||||
export default useIndexer;
|
||||
@@ -116,6 +116,26 @@ export const sortPredicates = {
|
||||
|
||||
vipExpiration: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
|
||||
},
|
||||
|
||||
minimumSeeders: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.appMinimumSeeders')?.value ?? undefined;
|
||||
},
|
||||
|
||||
seedRatio: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')?.value ?? undefined;
|
||||
},
|
||||
|
||||
seedTime: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.seedTime')?.value ?? undefined;
|
||||
},
|
||||
|
||||
packSeedTime: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')?.value ?? undefined;
|
||||
},
|
||||
|
||||
preferMagnetUrl: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')?.value ?? undefined;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -116,6 +116,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'preferMagnetUrl',
|
||||
label: () => translate('PreferMagnetUrl'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: () => translate('Tags'),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
let i = 0;
|
||||
|
||||
// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
|
||||
|
||||
/**
|
||||
* @deprecated Use React's useId() instead
|
||||
* @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
|
||||
*/
|
||||
export default function getUniqueElementId() {
|
||||
return `id-${i++}`;
|
||||
}
|
||||
|
||||
78
package.json
78
package.json
@@ -30,28 +30,27 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.100.0",
|
||||
"@sentry/integrations": "7.100.0",
|
||||
"@types/node": "18.19.31",
|
||||
"@sentry/browser": "7.119.1",
|
||||
"@sentry/integrations": "7.119.1",
|
||||
"@types/node": "20.16.11",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"chart.js": "4.4.3",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"chart.js": "4.4.4",
|
||||
"classnames": "2.5.1",
|
||||
"connected-react-router": "6.9.3",
|
||||
"copy-to-clipboard": "3.3.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.0.7",
|
||||
"filesize": "10.1.6",
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.7.1",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.4",
|
||||
"moment": "2.30.1",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.1",
|
||||
"qs": "6.13.0",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
@@ -65,7 +64,6 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"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",
|
||||
@@ -75,55 +73,55 @@
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-use-measure": "2.1.1",
|
||||
"react-virtualized": "9.21.1",
|
||||
"react-window": "1.8.8",
|
||||
"react-window": "1.8.10",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.7",
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.2",
|
||||
"@babel/eslint-parser": "7.25.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.7",
|
||||
"@babel/core": "7.25.8",
|
||||
"@babel/eslint-parser": "7.25.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.8",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.25.3",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@types/lodash": "4.14.194",
|
||||
"@types/react-document-title": "2.0.9",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@babel/preset-react": "7.25.7",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-document-title": "2.0.10",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.14.1",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/webpack-livereload-plugin": "2.3.3",
|
||||
"@types/react-text-truncate": "0.19.0",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/webpack-livereload-plugin": "2.3.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"are-you-es5": "2.1.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.38.0",
|
||||
"core-js": "3.38.1",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"eslint-plugin-react": "7.37.1",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"html-webpack-plugin": "5.6.0",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.41",
|
||||
"mini-css-extract-plugin": "2.9.1",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
@@ -132,17 +130,15 @@
|
||||
"postcss-url": "10.1.3",
|
||||
"prettier": "2.8.8",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "4.4.1",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"rimraf": "6.0.1",
|
||||
"style-loader": "3.3.2",
|
||||
"stylelint": "15.6.1",
|
||||
"stylelint-order": "6.0.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-loader": "9.4.2",
|
||||
"stylelint-order": "6.0.4",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-loader": "9.5.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.94.0",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://beyond-hd.me/torrent/download/the-next-365-days-2022-2160p-nf-web-dl-dual-ddp-51-dovi-hdr-hevc-apex.225146.2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"https://anthelion.me/api.php?api_key=2b51db35e1910123321025a12b9933d2&o=json&t=movie&q=&tmdb=&imdb=&cat=&limit=100&offset=0")]
|
||||
[TestCase(@"https://avistaz.to/api/v1/jackett/auth: username=mySecret&password=mySecret&pid=mySecret")]
|
||||
[TestCase(@"https://www.sharewood.tv/api/2b51db35e1910123321025a12b9933d2/last-torrents")]
|
||||
|
||||
// Indexer and Download Client Responses
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new (@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?:sharewood)\.[a-z]{2,3}/api/(?<secret>[a-z0-9]{16,})/", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// UNIT3D
|
||||
new (@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<PackageReference Include="NLog" Version="5.3.3" />
|
||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.12" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.7" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.8" />
|
||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Lidarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Lidarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Lidarr URL is invalid, Prowlarr cannot connect to Lidarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Radarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Radarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Radarr URL is invalid, Prowlarr cannot connect to Radarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -179,6 +179,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Readarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Readarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Readarr URL is invalid, Prowlarr cannot connect to Readarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -153,6 +153,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Sonarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Sonarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Sonarr URL is invalid, Prowlarr cannot connect to Sonarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Whisparr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Whisparr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Whisparr URL is invalid, Prowlarr cannot connect to Whisparr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -151,6 +151,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
return new XElement(feedNamespace + "attr", new XAttribute("name", name), new XAttribute("value", value));
|
||||
}
|
||||
|
||||
public string ToXml(DownloadProtocol protocol)
|
||||
public string ToXml(DownloadProtocol protocol, bool preferMagnetUrl = false)
|
||||
{
|
||||
// IMPORTANT: We can't use Uri.ToString(), because it generates URLs without URL encode (links with unicode
|
||||
// characters are broken). We must use Uri.AbsoluteUri instead that handles encoding correctly
|
||||
@@ -73,6 +73,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
new XElement("title", "Prowlarr"),
|
||||
from r in Releases
|
||||
let t = (r as TorrentInfo) ?? new TorrentInfo()
|
||||
let downloadUrl = preferMagnetUrl ? t.MagnetUrl ?? r.DownloadUrl : r.DownloadUrl ?? t.MagnetUrl
|
||||
select new XElement("item",
|
||||
new XElement("title", RemoveInvalidXMLChars(r.Title)),
|
||||
new XElement("description", RemoveInvalidXMLChars(r.Description)),
|
||||
@@ -85,11 +86,11 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl),
|
||||
r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)),
|
||||
new XElement("size", r.Size),
|
||||
new XElement("link", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty),
|
||||
new XElement("link", downloadUrl ?? string.Empty),
|
||||
r.Categories == null ? null : from c in r.Categories select new XElement("category", c.Id),
|
||||
new XElement(
|
||||
"enclosure",
|
||||
new XAttribute("url", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty),
|
||||
new XAttribute("url", downloadUrl ?? string.Empty),
|
||||
r.Size == null ? null : new XAttribute("length", r.Size),
|
||||
new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")),
|
||||
r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol),
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -94,8 +95,10 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
|
||||
indexerList = response.Resource.Where(i => !_definitionBlocklist.Contains(i.File)).ToList();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Error while getting indexer definitions, fallback to reading from disk.");
|
||||
|
||||
var definitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions");
|
||||
|
||||
indexerList = ReadDefinitionsFromDisk(indexerList, definitionFolder);
|
||||
@@ -106,9 +109,9 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
indexerList = ReadDefinitionsFromDisk(indexerList, customDefinitionFolder);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Failed to Connect to Indexer Definition Server for Indexer listing");
|
||||
_logger.Error(ex, "Failed to Connect to Indexer Definition Server for Indexer listing");
|
||||
}
|
||||
|
||||
return indexerList;
|
||||
@@ -116,7 +119,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
public CardigannDefinition GetCachedDefinition(string fileKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileKey))
|
||||
if (string.IsNullOrWhiteSpace(fileKey))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileKey));
|
||||
}
|
||||
@@ -172,7 +175,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
private CardigannDefinition GetUncachedDefinition(string fileKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileKey))
|
||||
if (string.IsNullOrWhiteSpace(fileKey))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileKey));
|
||||
}
|
||||
@@ -220,9 +223,24 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
private CardigannDefinition GetHttpDefinition(string id)
|
||||
{
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
|
||||
var response = _httpClient.Get(request);
|
||||
var definition = _deserializer.Deserialize<CardigannDefinition>(response.Content);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
CardigannDefinition definition;
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
|
||||
var response = _httpClient.Get(request);
|
||||
|
||||
definition = _deserializer.Deserialize<CardigannDefinition>(response.Content);
|
||||
}
|
||||
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new Exception($"Indexer definition for '{id}' does not exist.", ex);
|
||||
}
|
||||
|
||||
return CleanIndexerDefinition(definition);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList();
|
||||
|
||||
if (queryCats.Any() && searchCriteria is TvSearchCriteria { Season: > 0 })
|
||||
{
|
||||
// Avoid searching for specials if it's a non-zero season search
|
||||
queryCats.RemoveAll(cat => cat is "anime[tv_special]" or "anime[ova]" or "anime[ona]" or "anime[dvd_special]" or "anime[bd_special]");
|
||||
}
|
||||
|
||||
if (queryCats.Any())
|
||||
{
|
||||
@@ -246,9 +252,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
searchUrl += "?" + parameters.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
|
||||
yield return request;
|
||||
yield return new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
}
|
||||
|
||||
private static string CleanSearchTerm(string term)
|
||||
|
||||
@@ -33,6 +33,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
STJson.TryDeserialize<AvistazErrorResponse>(indexerResponse.HttpResponse.Content, out var errorResponse);
|
||||
throw new IndexerAuthException(errorResponse?.Message ?? "Unauthorized request to indexer");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
|
||||
@@ -27,16 +27,16 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "IndexerAvistazSettingsUsernameHelpText", HelpTextWarning = "IndexerAvistazSettingsUsernameHelpTextWarning", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "IndexerAvistazSettingsPasswordHelpText", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "IndexerAvistazSettingsPidHelpText", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Pid { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")]
|
||||
[FieldDefinition(5, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerAvistazSettingsFreeleechOnlyHelpText")]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new BeyondHDParser(Capabilities.Categories);
|
||||
return new BeyondHDParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
@@ -227,10 +227,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class BeyondHDParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly BeyondHDSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public BeyondHDParser(IndexerCapabilitiesCategories categories)
|
||||
public BeyondHDParser(BeyondHDSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
@@ -264,6 +266,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
foreach (var row in jsonResponse.Results)
|
||||
{
|
||||
// Skip invalid results when freeleech or limited filtering is set
|
||||
if ((_settings.FreeleechOnly && !row.Freeleech) || (_settings.LimitedOnly && !row.Limited))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var details = row.InfoUrl;
|
||||
var link = row.DownloadLink;
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
var loginUrl = ResolvePath(login.Path).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)).ToString();
|
||||
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
@@ -253,7 +253,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
else if (login.Method == "form")
|
||||
{
|
||||
var loginUrl = ResolvePath(login.Path).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)).ToString();
|
||||
|
||||
var queryCollection = new NameValueCollection();
|
||||
var pairs = new Dictionary<string, string>();
|
||||
@@ -534,7 +534,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
var loginUrl = ResolvePath(login.Path + "?" + queryCollection.GetQueryString()).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables) + "?" + queryCollection.GetQueryString()).ToString();
|
||||
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
@@ -563,7 +563,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
else if (login.Method == "oneurl")
|
||||
{
|
||||
var oneUrl = ApplyGoTemplateText(login.Inputs["oneurl"]);
|
||||
var loginUrl = ResolvePath(login.Path + oneUrl).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables) + oneUrl).ToString();
|
||||
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
@@ -639,7 +639,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
var variables = GetBaseTemplateVariables();
|
||||
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
|
||||
|
||||
var loginUrl = ResolvePath(login.Path);
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables));
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(loginUrl.AbsoluteUri)
|
||||
{
|
||||
@@ -700,7 +700,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
|
||||
if (captchaElement != null)
|
||||
{
|
||||
var loginUrl = ResolvePath(login.Path);
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables));
|
||||
var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl);
|
||||
|
||||
var request = new HttpRequestBuilder(captchaUrl.ToString())
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
{
|
||||
public class CardigannSettingsValidator : NoAuthSettingsValidator<CardigannSettings>
|
||||
{
|
||||
public CardigannSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.DefinitionFile).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class CardigannSettings : NoAuthTorrentBaseSettings
|
||||
{
|
||||
private static readonly CardigannSettingsValidator Validator = new ();
|
||||
|
||||
public CardigannSettings()
|
||||
{
|
||||
ExtraFieldData = new Dictionary<string, object>();
|
||||
@@ -15,5 +27,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
public string DefinitionFile { get; set; }
|
||||
|
||||
public Dictionary<string, object> ExtraFieldData { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ public class FileList : TorrentIndexerBase<FileListSettings>
|
||||
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.Movies3D, "Filme 3D");
|
||||
caps.Categories.AddCategoryMapping(26, NewznabStandardCategory.MoviesBluRay, "Filme 4K Blu-Ray");
|
||||
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.TVUHD, "Seriale 4K");
|
||||
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.MoviesForeign, "RO Dubbed");
|
||||
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.TVForeign, "RO Dubbed");
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
|
||||
if (searchCriteria.Categories != null && searchCriteria.Categories.Any())
|
||||
{
|
||||
parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories)));
|
||||
parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList()));
|
||||
}
|
||||
|
||||
if (Settings.FreeleechOnly)
|
||||
|
||||
@@ -22,6 +22,7 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
@@ -51,7 +52,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new MyAnonamouseParser(Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger);
|
||||
return new MyAnonamouseParser(Definition, Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger);
|
||||
}
|
||||
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
@@ -374,6 +375,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class MyAnonamouseParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly ProviderDefinition _definition;
|
||||
private readonly MyAnonamouseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly IIndexerHttpClient _httpClient;
|
||||
@@ -386,12 +388,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
"Elite VIP"
|
||||
};
|
||||
|
||||
public MyAnonamouseParser(MyAnonamouseSettings settings,
|
||||
public MyAnonamouseParser(ProviderDefinition definition,
|
||||
MyAnonamouseSettings settings,
|
||||
IndexerCapabilitiesCategories categories,
|
||||
IIndexerHttpClient httpClient,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_definition = definition;
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_httpClient = httpClient;
|
||||
@@ -543,7 +547,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
_logger.Debug("Fetching user data: {0}", request.Url.FullUri);
|
||||
|
||||
var response = _httpClient.Get(request);
|
||||
var response = _httpClient.ExecuteProxied(request, _definition);
|
||||
var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseUserDataResponse>(response.Content);
|
||||
|
||||
return jsonResponse.UserClass?.Trim();
|
||||
|
||||
@@ -73,6 +73,6 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
public class PassThePopcornFlag : IndexerFlag
|
||||
{
|
||||
public static IndexerFlag Golden => new ("golden", "Release follows Golden Popcorn quality rules");
|
||||
public static IndexerFlag Approved => new ("approved", "Release approved by PTP");
|
||||
public static IndexerFlag Approved => new ("approved", "Release approved by PTP staff");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,11 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
parameters.Set("freetorrent", "1");
|
||||
}
|
||||
|
||||
if (_settings.GoldenPopcornOnly)
|
||||
{
|
||||
parameters.Set("scene", "2");
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories
|
||||
.MapTorznabCapsToTrackers(searchCriteria.Categories)
|
||||
.Select(int.Parse)
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
[FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", HelpText = "IndexerPassThePopcornSettingsFreeleechOnlyHelpText", Type = FieldType.Checkbox)]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "IndexerPassThePopcornSettingsGoldenPopcornOnly", HelpText = "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText", Type = FieldType.Checkbox, Advanced = true)]
|
||||
public bool GoldenPopcornOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -63,5 +63,8 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Number, Label = "IndexerSettingsPackSeedTime", HelpText = "IndexerSettingsPackSeedTimeIndexerHelpText", Unit = "minutes", Advanced = true)]
|
||||
public int? PackSeedTime { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsPreferMagnetUrl", HelpText = "IndexerSettingsPreferMagnetUrlHelpText", Advanced = true)]
|
||||
public bool PreferMagnetUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,5 +398,8 @@
|
||||
"Script": "Manuskript",
|
||||
"BuiltIn": "Indbygget",
|
||||
"PublishedDate": "Udgivelsesdato",
|
||||
"AllSearchResultsHiddenByFilter": "Alle resultater skjules af det anvendte filter"
|
||||
"AllSearchResultsHiddenByFilter": "Alle resultater skjules af det anvendte filter",
|
||||
"AddApplication": "Tilføj Applikation",
|
||||
"AddCategory": "Tilføj Kategori",
|
||||
"ActiveApps": "Aktive Apps"
|
||||
}
|
||||
|
||||
@@ -320,6 +320,11 @@
|
||||
"IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Search freeleech releases only",
|
||||
"IndexerAlreadySetup": "At least one instance of indexer is already setup",
|
||||
"IndexerAuth": "Indexer Auth",
|
||||
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Search freeleech releases only",
|
||||
"IndexerAvistazSettingsPasswordHelpText": "Site Password",
|
||||
"IndexerAvistazSettingsPidHelpText": "PID from My Account or My Profile page",
|
||||
"IndexerAvistazSettingsUsernameHelpText": "Site Username",
|
||||
"IndexerAvistazSettingsUsernameHelpTextWarning": "Only member rank and above can use the API on this indexer.",
|
||||
"IndexerBeyondHDSettingsApiKeyHelpText": "API Key from the Site (Found in My Security => API Key)",
|
||||
"IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Search freeleech releases only",
|
||||
"IndexerBeyondHDSettingsLimitedOnly": "Limited Only",
|
||||
@@ -380,6 +385,8 @@
|
||||
"IndexerPassThePopcornSettingsApiKeyHelpText": "Site API Key",
|
||||
"IndexerPassThePopcornSettingsApiUserHelpText": "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).",
|
||||
"IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Search freeleech releases only",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "Golden Popcorn only",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Search Golden Popcorn releases only",
|
||||
"IndexerPriority": "Indexer Priority",
|
||||
"IndexerPriorityHelpText": "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25.",
|
||||
"IndexerProxies": "Indexer Proxies",
|
||||
@@ -407,6 +414,8 @@
|
||||
"IndexerSettingsPackSeedTime": "Pack Seed Time",
|
||||
"IndexerSettingsPackSeedTimeIndexerHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default",
|
||||
"IndexerSettingsPasskey": "Pass Key",
|
||||
"IndexerSettingsPreferMagnetUrl": "Prefer Magnet URL",
|
||||
"IndexerSettingsPreferMagnetUrlHelpText": "When enabled, this indexer will prefer the use of magnet URLs for grabs with fallback to torrent links",
|
||||
"IndexerSettingsQueryLimit": "Query Limit",
|
||||
"IndexerSettingsQueryLimitHelpText": "The number of max queries as specified by the respective unit that {appName} will allow to the site",
|
||||
"IndexerSettingsRssKey": "RSS Key",
|
||||
@@ -541,6 +550,8 @@
|
||||
"PendingChangesStayReview": "Stay and review changes",
|
||||
"Port": "Port",
|
||||
"PortNumber": "Port Number",
|
||||
"PreferMagnetUrl": "Prefer Magnet URL",
|
||||
"PreferMagnetUrlHelpText": "When enabled, this indexer will prefer the use of magnet URLs for grabs with fallback to torrent links",
|
||||
"Presets": "Presets",
|
||||
"Priority": "Priority",
|
||||
"PrioritySettings": "Priority: {priority}",
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
"URLBase": "URL base",
|
||||
"Uptime": "Tiempo de actividad",
|
||||
"UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización",
|
||||
"UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script",
|
||||
"UpdateMechanismHelpText": "Usa el actualizador integrado de {appName} o un script",
|
||||
"UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones",
|
||||
"UnableToLoadTags": "No se pueden cargar las Etiquetas",
|
||||
"UnableToLoadNotifications": "No se pueden cargar las Notificaciones",
|
||||
@@ -773,5 +773,16 @@
|
||||
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
|
||||
"HealthMessagesInfoBox": "Puede encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando sus [logs]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro servicio de asistencia en los enlaces que aparecen a continuación.",
|
||||
"LogSizeLimit": "Límite de tamaño de registro",
|
||||
"LogSizeLimitHelpText": "Máximo tamaño de archivo de registro en MB antes de archivarlo. Predeterminado es 1MB."
|
||||
"LogSizeLimitHelpText": "Máximo tamaño de archivo de registro en MB antes de archivarlo. Predeterminado es 1MB.",
|
||||
"PreferMagnetUrl": "Preferir URL magnet",
|
||||
"IndexerSettingsPreferMagnetUrl": "Preferir URL magnet",
|
||||
"IndexerSettingsPreferMagnetUrlHelpText": "Cuando está habilitado, este indexador preferirá el uso de URL magnet para capturas con alternativas a enlaces torrent",
|
||||
"PreferMagnetUrlHelpText": "Cuando está habilitado, este indexador preferirá el uso de URL magnet para capturas con alternativas a enlaces torrent",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "Solo Golden Popcorn",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Busca lanzamientos solo en Golden Popcorn",
|
||||
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech",
|
||||
"IndexerAvistazSettingsUsernameHelpText": "Nombre de usuario del sitio",
|
||||
"IndexerAvistazSettingsUsernameHelpTextWarning": "Solo los miembros de rango y superiores pueden usar la API en este indexador.",
|
||||
"IndexerAvistazSettingsPasswordHelpText": "Contraseña del sitio",
|
||||
"IndexerAvistazSettingsPidHelpText": "PID de la página de Mi cuenta o Mi perfil"
|
||||
}
|
||||
|
||||
@@ -773,5 +773,7 @@
|
||||
"PackageVersionInfo": "{packageVersion} par {packageAuthor}",
|
||||
"HealthMessagesInfoBox": "Vous pouvez trouver plus d'informations sur la cause de ces messages de contrôle de santé en cliquant sur le lien wiki (icône de livre) à la fin de la ligne, ou en vérifiant vos [journaux]({link}). Si vous rencontrez des difficultés pour interpréter ces messages, vous pouvez contacter notre support, via les liens ci-dessous.",
|
||||
"LogSizeLimit": "Limite de taille du journal",
|
||||
"LogSizeLimitHelpText": "Taille maximale du fichier journal en Mo avant archivage. La valeur par défaut est de 1 Mo."
|
||||
"LogSizeLimitHelpText": "Taille maximale du fichier journal en Mo avant archivage. La valeur par défaut est de 1 Mo.",
|
||||
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement",
|
||||
"IndexerAvistazSettingsUsernameHelpText": "Nom d'utilisateur du site"
|
||||
}
|
||||
|
||||
@@ -181,5 +181,32 @@
|
||||
"Directory": "Direktorij",
|
||||
"BuiltIn": "Ugrađeno",
|
||||
"Redirected": "Preusmjeri",
|
||||
"AllSearchResultsHiddenByFilter": "Svi rezultati su skriveni zbog primjenjenog filtera"
|
||||
"AllSearchResultsHiddenByFilter": "Svi rezultati su skriveni zbog primjenjenog filtera",
|
||||
"ApplyChanges": "Primjeni Promjene",
|
||||
"ApiKeyValidationHealthCheckMessage": "Molimo ažuriraj svoj API ključ da ima barem {length} znakova. Ovo možeš uraditi u postavkama ili konfiguracijskoj datoteci",
|
||||
"AppUpdated": "{appName} Ažuriran",
|
||||
"AuthenticationRequired": "Potrebna Autentikacija",
|
||||
"AddConnection": "Dodaj vezu",
|
||||
"AddDownloadClientImplementation": "Dodaj Klijenta za Preuzimanje- {implementationName}",
|
||||
"AddIndexerImplementation": "Dodaj Indexer - {implementationName}",
|
||||
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potvrdi novu lozinku",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "Unesi novu lozinku",
|
||||
"AddConnectionImplementation": "Dodaj Vezu - {implementationName}",
|
||||
"Any": "BIlo koji",
|
||||
"AppUpdatedVersion": "{appName} je ažuriran na verziju '{version}', kako bi najnovije promjene bile aktivne potrebno je ponovno učitati {appName}",
|
||||
"AuthenticationMethod": "Metoda Autentikacije",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Unesi novo korisničko ime",
|
||||
"AuthenticationMethodHelpTextWarning": "Molimo odaberi ispravnu metodu autentikacije",
|
||||
"AuthenticationRequiredWarning": "Kako bi se spriječio udaljeni pristup bez autentikacije, {appName} sad zahtjeva da autentikacija bude omogućena. Izborno se može onemogućiti autentikacija s lokalnih adresa.",
|
||||
"UnableToAddANewIndexerPleaseTryAgain": "Neuspješno dodavanje novog indexera, molimo pokušaj ponovno.",
|
||||
"UnableToAddANewNotificationPleaseTryAgain": "Neuspješno dodavanje nove obavijesti, molimo pokušaj ponovno.",
|
||||
"UnableToAddANewDownloadClientPleaseTryAgain": "Nesupješno dodavanje klijenta za preuzimanje, molimo pokušaj ponovno.",
|
||||
"EditDownloadClientImplementation": "Dodaj Klijenta za Preuzimanje- {implementationName}",
|
||||
"EditConnectionImplementation": "Dodaj Vezu - {implementationName}",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "Neuspješno dodavanje novog indexera, molimo pokušaj ponovno.",
|
||||
"AddApplicationImplementation": "Dodaj Vezu - {implementationName}",
|
||||
"UnableToAddANewAppProfilePleaseTryAgain": "Neuspješno dodavanje novog profila kvalitete, molimo pokušaj ponovno.",
|
||||
"EditIndexerImplementation": "Dodaj Indexer - {implementationName}",
|
||||
"AddIndexerProxyImplementation": "Dodaj Indexer - {implementationName}",
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "Neuspješno dodavanje nove obavijesti, molimo pokušaj ponovno."
|
||||
}
|
||||
|
||||
@@ -773,5 +773,16 @@
|
||||
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
|
||||
"HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.",
|
||||
"LogSizeLimit": "Limite de Tamanho do Registro",
|
||||
"LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB."
|
||||
"LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB.",
|
||||
"PreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
|
||||
"IndexerSettingsPreferMagnetUrl": "Preferir URL Magnético",
|
||||
"IndexerSettingsPreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
|
||||
"PreferMagnetUrl": "Preferir URL Magnético",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "Apenas Golden Popcorn",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Pesquisar somente lançamentos em Golden Popcorn",
|
||||
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech",
|
||||
"IndexerAvistazSettingsUsernameHelpText": "Nome de Usuário do Site",
|
||||
"IndexerAvistazSettingsPasswordHelpText": "Senha do Site",
|
||||
"IndexerAvistazSettingsPidHelpText": "PID da página Minha Conta ou Meu Perfil",
|
||||
"IndexerAvistazSettingsUsernameHelpTextWarning": "Somente membros com rank e acima podem usar a API neste indexador."
|
||||
}
|
||||
|
||||
@@ -773,5 +773,16 @@
|
||||
"UnableToLoadDevelopmentSettings": "Не удалось загрузить настройки разработки",
|
||||
"VipExpiration": "Дата окончания VIP",
|
||||
"IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent",
|
||||
"AverageGrabs": "Среднее количество захватов"
|
||||
"AverageGrabs": "Среднее количество захватов",
|
||||
"IndexerSettingsPreferMagnetUrl": "Предпочитать Magnet URL",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "Только Golden Popcorn",
|
||||
"IndexerSettingsPreferMagnetUrlHelpText": "При включении этот индексатор предпочтёт использовать для загрузки magnet URL, с возможностью перехода на торрент-ссылки",
|
||||
"IndexerAvistazSettingsPasswordHelpText": "Пароль веб-сайта",
|
||||
"IndexerAvistazSettingsPidHelpText": "PID со страницы Мой аккаунт или Мой профиль",
|
||||
"IndexerAvistazSettingsUsernameHelpTextWarning": "API этого индексатора доступен только для участников и выше рангом.",
|
||||
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Искать релизы только Golden Popcorn",
|
||||
"PreferMagnetUrl": "Предпочитать Magnet URL",
|
||||
"PreferMagnetUrlHelpText": "При включении этот индексатор предпочтёт использовать для загрузки magnet URL, с возможностью перехода на торрент-ссылки",
|
||||
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Искать только релизы freeleech",
|
||||
"IndexerAvistazSettingsUsernameHelpText": "Имя пользователя сайта"
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"DevelopmentSettings": "开发设置",
|
||||
"Disabled": "禁用",
|
||||
"DisabledUntil": "禁用Until",
|
||||
"Discord": "分歧",
|
||||
"Discord": "Discord",
|
||||
"Docker": "Docker",
|
||||
"Donations": "赞助",
|
||||
"DownloadClient": "下载客户端",
|
||||
@@ -368,7 +368,7 @@
|
||||
"Season": "季",
|
||||
"Security": "安全",
|
||||
"Seeders": "种子",
|
||||
"SelectAll": "选择全部",
|
||||
"SelectAll": "全选",
|
||||
"SemiPrivate": "半私有",
|
||||
"SendAnonymousUsageData": "发送匿名使用数据",
|
||||
"SetTags": "设置标签",
|
||||
@@ -389,13 +389,13 @@
|
||||
"SettingsShowRelativeDatesHelpText": "显示相对日期(今天昨天等)或绝对日期",
|
||||
"SettingsSqlLoggingHelpText": "记录来自{appName}的所有SQL查询",
|
||||
"SettingsTimeFormat": "时间格式",
|
||||
"ShowAdvanced": "显示高级设置",
|
||||
"ShowAdvanced": "高级设置",
|
||||
"ShowSearch": "显示搜索",
|
||||
"ShowSearchHelpText": "悬停时显示搜索按钮",
|
||||
"Shutdown": "关机",
|
||||
"Size": "大小",
|
||||
"Sort": "排序",
|
||||
"Source": "来源",
|
||||
"Source": "代码",
|
||||
"StartTypingOrSelectAPathBelow": "输入路径或者从下面选择",
|
||||
"Started": "已开始",
|
||||
"StartupDirectory": "启动目录",
|
||||
@@ -462,7 +462,7 @@
|
||||
"UnableToLoadTags": "无法加载标签",
|
||||
"UnableToLoadUISettings": "无法加载UI设置",
|
||||
"UnsavedChanges": "未保存更改",
|
||||
"UnselectAll": "取消选择全部",
|
||||
"UnselectAll": "取消全选",
|
||||
"UpdateAutomaticallyHelpText": "自动下载并安装更新。你还可以在“系统:更新”中安装",
|
||||
"UpdateAvailableHealthCheckMessage": "有新的更新可用",
|
||||
"UpdateStartupNotWritableHealthCheckMessage": "无法安装更新,因为用户“{userName}”对于启动文件夹“{startupFolder}”没有写入权限。",
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.151" />
|
||||
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
|
||||
<PackageReference Include="MailKit" Version="3.6.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.32" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.35" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.7" />
|
||||
<PackageReference Include="Polly" Version="8.4.1" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.8" />
|
||||
<PackageReference Include="Polly" Version="8.4.2" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
@@ -21,7 +21,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.3.3" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.10" />
|
||||
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
||||
<PackageReference Include="YamlDotNet" Version="13.1.1" />
|
||||
<PackageReference Include="AngleSharp" Version="1.1.2" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.32" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
public double? SeedRatio { get; set; }
|
||||
public int? SeedTime { get; set; }
|
||||
public int? PackSeedTime { get; set; }
|
||||
public bool? PreferMagnetUrl { get; set; }
|
||||
}
|
||||
|
||||
public class IndexerBulkResourceMapper : ProviderBulkResourceMapper<IndexerBulkResource, IndexerDefinition>
|
||||
@@ -35,6 +36,7 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio = resource.SeedRatio ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio;
|
||||
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime = resource.SeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime;
|
||||
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime = resource.PackSeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime;
|
||||
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PreferMagnetUrl = resource.PreferMagnetUrl ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PreferMagnetUrl;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -119,20 +119,29 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
|
||||
var settings = (CardigannSettings)definition.Settings;
|
||||
|
||||
var cardigannDefinition = _definitionService.GetCachedDefinition(settings.DefinitionFile);
|
||||
|
||||
foreach (var field in resource.Fields)
|
||||
if (settings.DefinitionFile.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
if (!standardFields.Contains(field.Name))
|
||||
var cardigannDefinition = _definitionService.GetCachedDefinition(settings.DefinitionFile);
|
||||
|
||||
foreach (var field in resource.Fields)
|
||||
{
|
||||
if (field.Name == "cardigannCaptcha")
|
||||
if (!standardFields.Contains(field.Name))
|
||||
{
|
||||
settings.ExtraFieldData["CAPTCHA"] = field.Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name);
|
||||
settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value);
|
||||
if (field.Name == "cardigannCaptcha")
|
||||
{
|
||||
settings.ExtraFieldData["CAPTCHA"] = field.Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name);
|
||||
|
||||
if (cardigannSetting == null)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(field.Name, "Unknown Cardigann setting.");
|
||||
}
|
||||
|
||||
settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,9 @@ namespace NzbDrone.Api.V1.Indexers
|
||||
}
|
||||
}
|
||||
|
||||
return CreateResponse(results.ToXml(indexer.Protocol));
|
||||
var preferMagnetUrl = indexer.Protocol == DownloadProtocol.Torrent && indexerDef.Settings is ITorrentIndexerSettings torrentIndexerSettings && (torrentIndexerSettings.TorrentBaseSettings?.PreferMagnetUrl ?? false);
|
||||
|
||||
return CreateResponse(results.ToXml(indexer.Protocol, preferMagnetUrl));
|
||||
default:
|
||||
return CreateResponse(CreateErrorXML(202, $"No such function ({requestType})"), statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
@@ -253,20 +255,25 @@ namespace NzbDrone.Api.V1.Indexers
|
||||
var source = Request.GetSource();
|
||||
var host = Request.GetHostName();
|
||||
|
||||
var unprotectedlLink = _downloadMappingService.ConvertToNormalLink(link);
|
||||
var unprotectedLink = _downloadMappingService.ConvertToNormalLink(link);
|
||||
|
||||
if (unprotectedLink.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("Failed to normalize provided link");
|
||||
}
|
||||
|
||||
// If Indexer is set to download via Redirect then just redirect to the link
|
||||
if (indexer.SupportsRedirect && indexerDef.Redirect)
|
||||
{
|
||||
_downloadService.RecordRedirect(unprotectedlLink, id, source, host, file);
|
||||
return RedirectPermanent(unprotectedlLink);
|
||||
_downloadService.RecordRedirect(unprotectedLink, id, source, host, file);
|
||||
return RedirectPermanent(unprotectedLink);
|
||||
}
|
||||
|
||||
byte[] downloadBytes;
|
||||
|
||||
try
|
||||
{
|
||||
downloadBytes = await _downloadService.DownloadReport(unprotectedlLink, id, source, host, file);
|
||||
downloadBytes = await _downloadService.DownloadReport(unprotectedLink, id, source, host, file);
|
||||
}
|
||||
catch (ReleaseUnavailableException ex)
|
||||
{
|
||||
|
||||
@@ -5089,6 +5089,10 @@
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"preferMagnetUrl": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace Prowlarr.Http.Middleware
|
||||
if (_urlBase.IsNotNullOrWhiteSpace() && context.Request.PathBase.Value.IsNullOrWhiteSpace())
|
||||
{
|
||||
context.Response.Redirect($"{_urlBase}{context.Request.Path}{context.Request.QueryString}");
|
||||
context.Response.StatusCode = 307;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user