Compare commits

..

50 Commits

Author SHA1 Message Date
Bogdan
3f547f0856 Cleanse exceptions in event logs 2024-10-20 13:39:43 +03:00
Bogdan
11e322b6d7 Bump version to 1.25.4 2024-10-20 08:05:32 +03:00
Weblate
02ff133a62 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-10-20 04:42:29 +03:00
Bogdan
47268aac87 Fix stable branch label in updates 2024-10-20 04:36:52 +03:00
Bogdan
8aad1ac554 New: Allow major version updates to be installed (#2260)
* New: Allow major version updates to be installed

(cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab)

* fixup! New: Allow major version updates to be installed

---------

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-10-19 09:01:23 +03:00
Bogdan
9037cde439 Rename ApplicationCheckUpdate to ApplicationUpdateCheck 2024-10-19 07:13:31 +03:00
Mark McDowall
2afafd79e4 Fixed: Don't block updates under docker unless configured in package_info
(cherry picked from commit 5a7e34e291c2715aa67161e5c455d25e80f498df)
2024-10-19 07:13:31 +03:00
Bogdan
f4fa2517d2 Sort indexers by name when syncing to applications 2024-10-18 23:12:57 +03:00
Stevie Robinson
37bc46c1cd Translate System pages
(cherry picked from commit 93e8ff0ac7610fa8739f2e577ece98c2c06c8881)
2024-10-18 11:44:20 +03:00
Weblate
3e3a7ed4f0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: JoseFilipeFerreira <jose.filipe.matos.ferreira@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translation: Servarr/Prowlarr
2024-10-16 04:09:51 +03:00
Bogdan
04fa7d366d Fixed: (Cardigann) Redirect warnings with "Refresh" header 2024-10-13 20:08:54 +03:00
Bogdan
ed9a3214a2 Fix redirect url in HttpClient development warning message 2024-10-13 19:56:32 +03:00
Bogdan
66a9e1a653 Bump dotnet to 6.0.35 2024-10-13 19:32:37 +03:00
Bogdan
8cb59c35fb New: Sync UI updates for providers 2024-10-13 07:23:10 +03:00
Bogdan
94e9c05d60 Natural sorting for tags list in the UI 2024-10-13 07:21:27 +03:00
Bogdan
8d2c4e1246 Bump version to 1.25.3 2024-10-13 07:20:52 +03:00
Bogdan
c05be39346 Treat unauthorized newbie accounts in AvistaZ parser 2024-10-12 22:21:05 +03:00
Bogdan
951d42a591 Fix indexer url info 2024-10-12 05:30:03 +03:00
Bogdan
dd046d8a68 Fixed: (Cardigann) Validate definition file and setting fields existence
Towards #2245
2024-10-11 19:23:30 +03:00
Bogdan
efa54a4d51 Remove unused gulp packages 2024-10-10 19:19:59 +03:00
Bogdan
3f07c50cc5 Fixed: Copy to clipboard in non-secure contexts
(cherry picked from commit 3828e475cc8860e74cdfd8a70b4f886de7f9c5c3)
2024-10-10 19:19:59 +03:00
Treycos
94cf07ddb4 Convert ClipboardButton to TypeScript
(cherry picked from commit 99fc52039f44264c83d939e5f096d8e16d2f3355)
2024-10-10 19:19:59 +03:00
Bogdan
24063e06ab Convert FormInputButton to TypeScript
(cherry picked from commit 32fa63d24d08d8d8877386a8d2e7065ab5d0ad39)
2024-10-10 19:19:59 +03:00
Treycos
e8ebb87189 Convert Label to TypeScript
(cherry picked from commit 3eca63a67c898256b711d37607f07cbabb9ed323)
2024-10-10 19:19:59 +03:00
Treycos
896e196767 Convert Button to TypeScript
(cherry picked from commit 63b4998c8e51d0d2b8b51133cbb1fd928394a7e6)
2024-10-10 19:19:59 +03:00
Bogdan
9f5be75e6d Link polymorphic static typing
(cherry picked from commit a2e06e9e650642518b926a61f624a2c7a49c0988)
(cherry picked from commit cfa2f4d4c6e35d7b9ddd2e1da2e59f7287859516)
2024-10-10 19:19:59 +03:00
Bogdan
9cc9e720bb Bump frontend packages 2024-10-10 19:19:59 +03:00
Bogdan
a9c2cca66d Bump dotnet packages 2024-10-09 23:56:11 +03:00
Bogdan
9cc3646be5 Fixed: (Cardigann) Using variables in login paths 2024-10-09 00:50:40 +03:00
Bogdan
d6bca449da Cleanse sharewood passkey 2024-10-09 00:26:08 +03:00
Bogdan
cb5764c654 Log exceptions when getting indexer definitions
Closes #2245
2024-10-08 01:44:36 +03:00
Weblate
19a9b56fa4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: Mathias <mathias@rodilbach.dk>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: jsain <josip.sain@gmail.com>
Co-authored-by: liuwqq <843384478@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-10-06 18:19:32 +03:00
Tiago Santos
a2b0f199f1 Fixed: (BeyondHD) Filter freeleech or limited releases when configured 2024-10-06 17:56:21 +03:00
Mark McDowall
59bfad7614 New: Use 307 redirect for requests missing URL Base 2024-10-06 17:48:02 +03:00
Bogdan
aee3f2d12b Fixed: Handle 307 redirects from applications 2024-10-06 17:47:48 +03:00
Bogdan
11d58b4460 Bump macOS runner version to 13 2024-10-06 16:22:06 +03:00
Bogdan
ee4de6c6ca Bump version to 1.25.2 2024-10-06 12:04:50 +03:00
Bogdan
8d16b88185 Return bad request for unprotect download link failures 2024-10-05 17:20:17 +03:00
Bogdan
121ef8e80d Add new category for FL 2024-09-30 17:26:31 +03:00
Bogdan
d53fec7e75 Add newbie warning for AvistaZ's API use 2024-09-30 11:21:36 +03:00
Bogdan
c017a3cd7e New: (PTP) Filter by Golden Popcorn only releases 2024-09-29 12:12:26 +03:00
Bogdan
27ea93090f Use proxied requests for fetching user class for MAM 2024-09-29 10:40:16 +03:00
Bogdan
d79845144e Bump version to 1.25.1 2024-09-29 08:17:56 +03:00
Servarr
3f77900dd0 Automated API Docs update 2024-09-27 15:59:16 +03:00
Bogdan
4e8b9e81cf New: Option to prefer magnet URLs over torrent file links
Co-authored-by: Deathspike <meister.deathspike@outlook.com>

New: Bulk edit Prefer Magnet Url for indexers
2024-09-27 06:42:06 +03:00
Bogdan
a32ab3acfd Fixed: (AnimeBytes) Avoid specials for non-zero season searches 2024-09-27 06:24:04 +03:00
Bogdan
942da3a5c0 Bump version to 1.25.0 2024-09-27 06:23:48 +03:00
Qstick
17e1a72baf Bump webpack to 5.94.0 and regenerate yarn.lock 2024-09-22 22:34:45 -05:00
Bogdan
b454ded00a Bump version to 1.24.3 2024-09-22 07:48:53 +03:00
Servarr
d4512393e2 Automated API Docs update 2024-09-21 22:05:08 +03:00
135 changed files with 3104 additions and 3012 deletions

View File

@@ -9,18 +9,18 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.24.2'
majorVersion: '1.25.4'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.424'
dotnetVersion: '6.0.427'
nodeVersion: '20.X'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-12'
macImage: 'macOS-13'
trigger:
branches:

View File

@@ -20,7 +20,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import Updates from 'System/Updates/Updates';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
function RedirectWithUrlBase() {
@@ -99,7 +99,7 @@ function AppRoutes() {
<Route path="/system/backup" component={BackupsConnector} />
<Route path="/system/updates" component={UpdatesConnector} />
<Route path="/system/updates" component={Updates} />
<Route path="/system/events" component={LogsTableConnector} />

View File

@@ -7,7 +7,8 @@ import { IndexerCategory } from 'Indexer/Indexer';
import Application from 'typings/Application';
import DownloadClient from 'typings/DownloadClient';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
import General from 'typings/Settings/General';
import UiSettings from 'typings/Settings/UiSettings';
export interface AppProfileAppState
extends AppSectionState<Application>,
@@ -28,6 +29,10 @@ export interface DownloadClientAppState
isTestingAll: boolean;
}
export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface IndexerCategoryAppState
extends AppSectionState<IndexerCategory>,
AppSectionDeleteState,
@@ -43,6 +48,7 @@ interface SettingsAppState {
appProfiles: AppProfileAppState;
applications: ApplicationAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
indexerCategories: IndexerCategoryAppState;
notifications: NotificationAppState;
ui: UiSettingsAppState;

View File

@@ -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;

View 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;

View File

@@ -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;

View 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}
/>
);
}

View File

@@ -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;

View 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}
/>
);
}

View File

@@ -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;

View 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>
);
}

View File

@@ -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;

View File

@@ -141,6 +141,16 @@ class SignalRConnector extends Component {
console.error(`signalR: Unable to find handler for ${name}`);
};
handleApplications = ({ action, resource }) => {
const section = 'settings.applications';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleCommand = (body) => {
if (body.action === 'sync') {
this.props.dispatchFetchCommands();
@@ -150,8 +160,8 @@ class SignalRConnector extends Component {
const resource = body.resource;
const status = resource.status;
// Both sucessful and failed commands need to be
// completed, otherwise they spin until they timeout.
// Both successful and failed commands need to be
// completed, otherwise they spin until they time out.
if (status === 'completed' || status === 'failed') {
this.props.dispatchFinishCommand(resource);
@@ -160,6 +170,16 @@ class SignalRConnector extends Component {
}
};
handleDownloadclient = ({ action, resource }) => {
const section = 'settings.downloadClients';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleHealth = () => {
this.props.dispatchFetchHealth();
};
@@ -168,14 +188,33 @@ class SignalRConnector extends Component {
this.props.dispatchFetchIndexerStatus();
};
handleIndexer = (body) => {
const action = body.action;
handleIndexer = ({ action, resource }) => {
const section = 'indexers';
if (action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource });
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleIndexerproxy = ({ action, resource }) => {
const section = 'settings.indexerProxies';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleNotification = ({ action, resource }) => {
const section = 'settings.notifications';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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}>

View File

@@ -29,7 +29,8 @@
.minimumSeeders,
.seedRatio,
.seedTime,
.packSeedTime {
.packSeedTime,
.preferMagnetUrl {
composes: cell;
flex: 0 0 90px;

View File

@@ -11,6 +11,7 @@ interface CssExports {
'id': string;
'minimumSeeders': string;
'packSeedTime': string;
'preferMagnetUrl': string;
'priority': string;
'privacy': string;
'protocol': string;

View File

@@ -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

View File

@@ -22,7 +22,8 @@
.minimumSeeders,
.seedRatio,
.seedTime,
.packSeedTime {
.packSeedTime,
.preferMagnetUrl {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 90px;

View File

@@ -8,6 +8,7 @@ interface CssExports {
'id': string;
'minimumSeeders': string;
'packSeedTime': string;
'preferMagnetUrl': string;
'priority': string;
'privacy': string;
'protocol': string;

View File

@@ -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>
);

View 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;

View File

@@ -4,11 +4,13 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import Tags from './Tags';
function createMapStateToProps() {
return createSelector(
(state) => state.tags,
createSortedSectionSelector('tags', sortByProp('label')),
(tags) => {
const isFetching = tags.isFetching || tags.details.isFetching;
const error = tags.error || tags.details.error;

View File

@@ -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;
}
};

View File

@@ -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'),

View File

@@ -116,6 +116,7 @@ class BackupRow extends Component {
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RestoreBackup')}
name={icons.RESTORE}
onPress={this.onRestorePress}
/>
@@ -138,7 +139,9 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', { name })}
message={translate('DeleteBackupMessageText', {
name
})}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose}

View File

@@ -109,7 +109,7 @@ class Backups extends Component {
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadBackups')}
{translate('BackupsLoadError')}
</Alert>
}

View File

@@ -14,7 +14,7 @@ import styles from './RestoreBackupModalContent.css';
function getErrorMessage(error) {
if (!error || !error.responseJSON || !error.responseJSON.message) {
return 'Error restoring backup';
return translate('ErrorRestoringBackup');
}
return error.responseJSON.message;
@@ -146,7 +146,9 @@ class RestoreBackupModalContent extends Component {
<ModalBody>
{
!!id && `Would you like to restore the backup '${name}'?`
!!id && translate('WouldYouLikeToRestoreBackup', {
name
})
}
{
@@ -203,7 +205,7 @@ class RestoreBackupModalContent extends Component {
<ModalFooter>
<div className={styles.additionalInfo}>
Note: Prowlarr will automatically restart and reload the UI during the restore process.
{translate('RestartReloadNote')}
</div>
<Button onPress={onModalClose}>
@@ -216,7 +218,7 @@ class RestoreBackupModalContent extends Component {
isSpinning={isRestoring}
onPress={this.onRestorePress}
>
Restore
{translate('Restore')}
</SpinnerButton>
</ModalFooter>
</ModalContent>

View File

@@ -84,7 +84,7 @@ function LogsTable(props) {
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
No events found
{translate('NoEventsFound')}
</Alert>
}

View File

@@ -28,7 +28,7 @@ function LogsTableDetailsModal(props) {
onModalClose={onModalClose}
>
<ModalHeader>
Details
{translate('Details')}
</ModalHeader>
<ModalBody>

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -77,13 +77,15 @@ class LogFiles extends Component {
<PageContentBody>
<Alert>
<div>
Log files are located in: {location}
{translate('LogFilesLocation', {
location
})}
</div>
{
currentLogView === 'Log Files' &&
<div>
The log level defaults to 'Info' and can be changed in <Link to="/settings/general">General Settings</Link>
<InlineMarkdown data={translate('TheLogLevelDefault')} />
</div>
}
</Alert>

View File

@@ -7,6 +7,7 @@ import { executeCommand } from 'Store/Actions/commandActions';
import { fetchLogFiles } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import combinePath from 'Utilities/String/combinePath';
import translate from 'Utilities/String/translate';
import LogFiles from './LogFiles';
function createMapStateToProps() {
@@ -29,7 +30,7 @@ function createMapStateToProps() {
isFetching,
items,
deleteFilesExecuting,
currentLogView: 'Log Files',
currentLogView: translate('LogFiles'),
location: combinePath(isWindows, appData, ['logs'])
};
}

View File

@@ -4,6 +4,7 @@ import Link from 'Components/Link/Link';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import translate from 'Utilities/String/translate';
import styles from './LogFilesTableRow.css';
class LogFilesTableRow extends Component {
@@ -32,7 +33,7 @@ class LogFilesTableRow extends Component {
target="_blank"
noRouter={true}
>
Download
{translate('Download')}
</Link>
</TableRowCell>
</TableRow>

View File

@@ -4,6 +4,7 @@ import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';
import translate from 'Utilities/String/translate';
class LogsNavMenu extends Component {
@@ -50,13 +51,13 @@ class LogsNavMenu extends Component {
<MenuItem
to={'/system/logs/files'}
>
Log Files
{translate('LogFiles')}
</MenuItem>
<MenuItem
to={'/system/logs/files/update'}
>
Updater Log Files
{translate('UpdaterLogFiles')}
</MenuItem>
</MenuContent>
</Menu>

View File

@@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import styles from './UpdateChanges.css';
class UpdateChanges extends Component {
//
// Render
render() {
const {
title,
changes
} = this.props;
if (changes.length === 0) {
return null;
}
const uniqueChanges = [...new Set(changes)];
return (
<div>
<div className={styles.title}>{title}</div>
<ul>
{
uniqueChanges.map((change, index) => {
const checkChange = change.replace(/#\d{3,5}\b/g, (match, contents) => {
return `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(1)})`;
});
return (
<li key={index}>
<InlineMarkdown data={checkChange} />
</li>
);
})
}
</ul>
</div>
);
}
}
UpdateChanges.propTypes = {
title: PropTypes.string.isRequired,
changes: PropTypes.arrayOf(PropTypes.string)
};
export default UpdateChanges;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import styles from './UpdateChanges.css';
interface UpdateChangesProps {
title: string;
changes: string[];
}
function UpdateChanges(props: UpdateChangesProps) {
const { title, changes } = props;
if (changes.length === 0) {
return null;
}
const uniqueChanges = [...new Set(changes)];
return (
<div>
<div className={styles.title}>{title}</div>
<ul>
{uniqueChanges.map((change, index) => {
const checkChange = change.replace(
/#\d{3,5}\b/g,
(match) =>
`[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(
1
)})`
);
return (
<li key={index}>
<InlineMarkdown data={checkChange} />
</li>
);
})}
</ul>
</div>
);
}
export default UpdateChanges;

View File

@@ -1,252 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import UpdateChanges from './UpdateChanges';
import styles from './Updates.css';
class Updates extends Component {
//
// Render
render() {
const {
currentVersion,
isFetching,
isPopulated,
updatesError,
generalSettingsError,
items,
isInstallingUpdate,
updateMechanism,
isDocker,
updateMechanismMessage,
shortDateFormat,
longDateFormat,
timeFormat,
onInstallLatestPress
} = this.props;
const hasError = !!(updatesError || generalSettingsError);
const hasUpdates = isPopulated && !hasError && items.length > 0;
const noUpdates = isPopulated && !hasError && !items.length;
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterPrefix = 'Unable to update Prowlarr directly,';
const externalUpdaterMessages = {
external: 'Prowlarr is configured to use an external update mechanism',
apt: 'use apt to install the update',
docker: 'update the docker container to receive the update'
};
return (
<PageContent title={translate('Updates')}>
<PageContentBody>
{
!isPopulated && !hasError &&
<LoadingIndicator />
}
{
noUpdates &&
<Alert kind={kinds.INFO}>
{translate('NoUpdatesAreAvailable')}
</Alert>
}
{
hasUpdateToInstall &&
<div className={styles.messageContainer}>
{
(updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ?
<SpinnerButton
className={styles.updateAvailable}
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress}
>
Install Latest
</SpinnerButton> :
<Fragment>
<Icon
name={icons.WARNING}
kind={kinds.WARNING}
size={30}
/>
<div className={styles.message}>
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
</div>
</Fragment>
}
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
noUpdateToInstall &&
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>
{translate('TheLatestVersionIsAlreadyInstalled')}
</div>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
</div>
}
{
hasUpdates &&
<div>
{
items.map((update) => {
const hasChanges = !!update.changes;
return (
<div
key={update.version}
className={styles.update}
>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{
update.branch === 'master' ?
null:
<Label
className={styles.label}
>
{update.branch}
</Label>
}
{
update.version === currentVersion ?
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
Currently Installed
</Label> :
null
}
{
update.version !== currentVersion && update.installedOn ?
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
Previously Installed
</Label> :
null
}
</div>
{
!hasChanges &&
<div>
{translate('MaintenanceRelease')}
</div>
}
{
hasChanges &&
<div className={styles.changes}>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
}
</div>
);
})
}
</div>
}
{
!!updatesError &&
<div>
Failed to fetch updates
</div>
}
{
!!generalSettingsError &&
<div>
Failed to update settings
</div>
}
</PageContentBody>
</PageContent>
);
}
}
Updates.propTypes = {
currentVersion: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
updatesError: PropTypes.object,
generalSettingsError: PropTypes.object,
items: PropTypes.array.isRequired,
isInstallingUpdate: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
updateMechanism: PropTypes.string,
updateMechanismMessage: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onInstallLatestPress: PropTypes.func.isRequired
};
export default Updates;

View File

@@ -0,0 +1,303 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import { fetchUpdates } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { UpdateMechanism } from 'typings/Settings/General';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import UpdateChanges from './UpdateChanges';
import styles from './Updates.css';
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
function createUpdatesSelector() {
return createSelector(
(state: AppState) => state.system.updates,
(state: AppState) => state.settings.general,
(updates, generalSettings) => {
const { error: updatesError, items } = updates;
const isFetching = updates.isFetching || generalSettings.isFetching;
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
return {
isFetching,
isPopulated,
updatesError,
generalSettingsError: generalSettings.error,
items,
updateMechanism: generalSettings.item.updateMechanism,
};
}
);
}
function Updates() {
const currentVersion = useSelector((state: AppState) => state.app.version);
const { packageUpdateMechanismMessage } = useSelector(
createSystemStatusSelector()
);
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const isInstallingUpdate = useSelector(
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
);
const {
isFetching,
isPopulated,
updatesError,
generalSettingsError,
items,
updateMechanism,
} = useSelector(createUpdatesSelector());
const dispatch = useDispatch();
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
const hasError = !!(updatesError || generalSettingsError);
const hasUpdates = isPopulated && !hasError && items.length > 0;
const noUpdates = isPopulated && !hasError && !items.length;
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
external: translate('ExternalUpdater'),
apt: translate('AptUpdater'),
docker: translate('DockerUpdater'),
};
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
const majorVersion = parseInt(
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
);
const latestVersion = items[0]?.version;
const latestMajorVersion = parseInt(
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
);
return {
isMajorUpdate: latestMajorVersion > majorVersion,
hasUpdateToInstall: items.some(
(update) => update.installable && update.latest
),
};
}, [currentVersion, items]);
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const handleInstallLatestPress = useCallback(() => {
if (isMajorUpdate) {
setIsMajorUpdateModalOpen(true);
} else {
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
}
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
const handleInstallLatestMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);
dispatch(
executeCommand({
name: commandNames.APPLICATION_UPDATE,
installMajorUpdate: true,
})
);
}, [setIsMajorUpdateModalOpen, dispatch]);
const handleCancelMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);
}, [setIsMajorUpdateModalOpen]);
useEffect(() => {
dispatch(fetchUpdates());
dispatch(fetchGeneralSettings());
}, [dispatch]);
return (
<PageContent title={translate('Updates')}>
<PageContentBody>
{isPopulated || hasError ? null : <LoadingIndicator />}
{noUpdates ? (
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
) : null}
{hasUpdateToInstall ? (
<div className={styles.messageContainer}>
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={handleInstallLatestPress}
>
{translate('InstallLatest')}
</SpinnerButton>
) : (
<>
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
<div className={styles.message}>
{externalUpdaterPrefix}{' '}
<InlineMarkdown
data={
packageUpdateMechanismMessage ||
externalUpdaterMessages[updateMechanism] ||
externalUpdaterMessages.external
}
/>
</div>
</>
)}
{isFetching ? (
<LoadingIndicator className={styles.loading} size={20} />
) : null}
</div>
) : null}
{noUpdateToInstall && (
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.message}>{translate('OnLatestVersion')}</div>
{isFetching && (
<LoadingIndicator className={styles.loading} size={20} />
)}
</div>
)}
{hasUpdates && (
<div>
{items.map((update) => {
return (
<div key={update.version} className={styles.update}>
<div className={styles.info}>
<div className={styles.version}>{update.version}</div>
<div className={styles.space}>&mdash;</div>
<div
className={styles.date}
title={formatDateTime(
update.releaseDate,
longDateFormat,
timeFormat
)}
>
{formatDate(update.releaseDate, shortDateFormat)}
</div>
{update.branch === 'master' ? null : (
<Label className={styles.label}>{update.branch}</Label>
)}
{update.version === currentVersion ? (
<Label
className={styles.label}
kind={kinds.SUCCESS}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('CurrentlyInstalled')}
</Label>
) : null}
{update.version !== currentVersion && update.installedOn ? (
<Label
className={styles.label}
kind={kinds.INVERSE}
title={formatDateTime(
update.installedOn,
longDateFormat,
timeFormat
)}
>
{translate('PreviouslyInstalled')}
</Label>
) : null}
</div>
{update.changes ? (
<div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
) : (
<div>{translate('MaintenanceRelease')}</div>
)}
</div>
);
})}
</div>
)}
{updatesError ? (
<Alert kind={kinds.WARNING}>
{translate('FailedToFetchUpdates')}
</Alert>
) : null}
{generalSettingsError ? (
<Alert kind={kinds.DANGER}>
{translate('FailedToUpdateSettings')}
</Alert>
) : null}
<ConfirmModal
isOpen={isMajorUpdateModalOpen}
kind={kinds.WARNING}
title={translate('InstallMajorVersionUpdate')}
message={
<div>
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
<div>
<InlineMarkdown
data={translate('InstallMajorVersionUpdateMessageLink', {
domain: 'prowlarr.com',
url: 'https://prowlarr.com/#downloads',
})}
/>
</div>
</div>
}
confirmLabel={translate('Install')}
onConfirm={handleInstallLatestMajorVersionPress}
onCancel={handleCancelMajorVersionPress}
/>
</PageContentBody>
</PageContent>
);
}
export default Updates;

View File

@@ -1,101 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import { fetchUpdates } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import Updates from './Updates';
function createMapStateToProps() {
return createSelector(
(state) => state.app.version,
createSystemStatusSelector(),
(state) => state.system.updates,
(state) => state.settings.general,
createUISettingsSelector(),
createSystemStatusSelector(),
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
(
currentVersion,
status,
updates,
generalSettings,
uiSettings,
systemStatus,
isInstallingUpdate
) => {
const {
error: updatesError,
items
} = updates;
const isFetching = updates.isFetching || generalSettings.isFetching;
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
return {
currentVersion,
isFetching,
isPopulated,
updatesError,
generalSettingsError: generalSettings.error,
items,
isInstallingUpdate,
isDocker: systemStatus.isDocker,
updateMechanism: generalSettings.item.updateMechanism,
updateMechanismMessage: status.packageUpdateMechanismMessage,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
const mapDispatchToProps = {
dispatchFetchUpdates: fetchUpdates,
dispatchFetchGeneralSettings: fetchGeneralSettings,
dispatchExecuteCommand: executeCommand
};
class UpdatesConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchUpdates();
this.props.dispatchFetchGeneralSettings();
}
//
// Listeners
onInstallLatestPress = () => {
this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE });
};
//
// Render
render() {
return (
<Updates
onInstallLatestPress={this.onInstallLatestPress}
{...this.props}
/>
);
}
}
UpdatesConnector.propTypes = {
dispatchFetchUpdates: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector);

View File

@@ -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++}`;
}

View File

@@ -0,0 +1,45 @@
export type UpdateMechanism =
| 'builtIn'
| 'script'
| 'external'
| 'apt'
| 'docker';
export default interface General {
bindAddress: string;
port: number;
sslPort: number;
enableSsl: boolean;
launchBrowser: boolean;
authenticationMethod: string;
authenticationRequired: string;
analyticsEnabled: boolean;
username: string;
password: string;
passwordConfirmation: string;
logLevel: string;
consoleLogLevel: string;
branch: string;
apiKey: string;
sslCertPath: string;
sslCertPassword: string;
urlBase: string;
instanceName: string;
applicationUrl: string;
updateAutomatically: boolean;
updateMechanism: UpdateMechanism;
updateScriptPath: string;
proxyEnabled: boolean;
proxyType: string;
proxyHostname: string;
proxyPort: number;
proxyUsername: string;
proxyPassword: string;
proxyBypassFilter: string;
proxyBypassLocalAddresses: boolean;
certificateValidation: string;
backupFolder: string;
backupInterval: number;
backupRetention: number;
id: number;
}

View File

@@ -1,4 +1,4 @@
export interface UiSettings {
export default interface UiSettings {
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean;
shortDateFormat: string;

View File

@@ -22,6 +22,7 @@ interface SystemStatus {
osVersion: string;
packageAuthor: string;
packageUpdateMechanism: string;
packageUpdateMechanismMessage: string;
packageVersion: string;
runtimeName: string;
runtimeVersion: string;

View File

@@ -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.89.0",
"webpack": "5.95.0",
"webpack-cli": "5.1.4",
"webpack-livereload-plugin": "3.0.2"
}

View File

@@ -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

View File

@@ -109,7 +109,7 @@ namespace NzbDrone.Common.Http
if (response.HasHttpRedirect && !RuntimeInfo.IsProduction)
{
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.RedirectUrl);
}
if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))

View File

@@ -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),

View File

@@ -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" />

View File

@@ -35,18 +35,18 @@ namespace NzbDrone.Core.Test.UpdateTests
{
_updatePackage = new UpdatePackage
{
FileName = "NzbDrone.develop.2.0.0.0.tar.gz",
FileName = "NzbDrone.develop.1.0.0.0.tar.gz",
Url = "http://download.sonarr.tv/v2/develop/mono/NzbDrone.develop.tar.gz",
Version = new Version("2.0.0.0")
Version = new Version("1.0.0.0")
};
}
else
{
_updatePackage = new UpdatePackage
{
FileName = "NzbDrone.develop.2.0.0.0.zip",
FileName = "NzbDrone.develop.1.0.0.0.zip",
Url = "http://download.sonarr.tv/v2/develop/windows/NzbDrone.develop.zip",
Version = new Version("2.0.0.0")
Version = new Version("1.0.0.0")
};
}
@@ -90,17 +90,6 @@ namespace NzbDrone.Core.Test.UpdateTests
.Returns(true);
}
[Test]
public void should_not_update_if_inside_docker()
{
Mocker.GetMock<IOsInfo>().Setup(x => x.IsDocker).Returns(true);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IProcessProvider>()
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Never());
}
[Test]
public void should_delete_sandbox_before_update_if_folder_exists()
{
@@ -338,6 +327,28 @@ namespace NzbDrone.Core.Test.UpdateTests
.Verify(v => v.SaveConfigDictionary(It.Is<Dictionary<string, object>>(d => d.ContainsKey("Branch") && (string)d["Branch"] == "fake")), Times.Once());
}
[Test]
public void should_not_update_with_built_in_updater_inside_docker_container()
{
Mocker.GetMock<IDeploymentInfoProvider>().Setup(x => x.PackageUpdateMechanism).Returns(UpdateMechanism.Docker);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IProcessProvider>()
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Never());
}
[Test]
public void should_not_update_with_built_in_updater_when_external_updater_is_configured()
{
Mocker.GetMock<IDeploymentInfoProvider>().Setup(x => x.IsExternalUpdateMechanism).Returns(true);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IProcessProvider>()
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Never());
}
[TearDown]
public void TearDown()
{

View File

@@ -127,6 +127,8 @@ namespace NzbDrone.Core.Applications
private void SyncIndexers(List<IApplication> applications, List<IndexerDefinition> indexers, bool removeRemote = false, bool forceSync = false)
{
var sortedIndexers = indexers.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList();
foreach (var app in applications)
{
var indexerMappings = _appIndexerMapService.GetMappingsForApp(app.Definition.Id);
@@ -157,7 +159,7 @@ namespace NzbDrone.Core.Applications
}
}
foreach (var indexer in indexers)
foreach (var indexer in sortedIndexers)
{
var definition = indexer;

View File

@@ -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;

View File

@@ -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:

View File

@@ -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;

View File

@@ -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:

View File

@@ -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;

View File

@@ -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:

View File

@@ -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;

View File

@@ -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:

View File

@@ -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;

View File

@@ -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:

View File

@@ -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),

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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");

View File

@@ -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()

View File

@@ -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;

View File

@@ -39,22 +39,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
var indexerLogging = _configService.LogIndexerResponse;
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
if (indexerResponse.HttpResponse.HasHttpRedirect && indexerResponse.HttpResponse.RedirectUrl.IsNotNullOrWhiteSpace())
{
if (indexerResponse.HttpResponse.HasHttpRedirect)
_logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl);
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php"))
{
_logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl);
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php"))
{
// Remove cookie cache
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
}
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request");
// Remove cookie cache
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
}
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request");
}
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
}

View File

@@ -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())

View File

@@ -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));
}
}
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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();

View File

@@ -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");
}
}

View File

@@ -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)

View File

@@ -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));

View File

@@ -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; }
}
}

View File

@@ -60,33 +60,36 @@ namespace NzbDrone.Core.Instrumentation
{
try
{
var log = new Log();
log.Time = logEvent.TimeStamp;
log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage);
log.Logger = logEvent.LoggerName;
var log = new Log
{
Time = logEvent.TimeStamp,
Logger = logEvent.LoggerName,
Level = logEvent.Level.Name
};
if (log.Logger.StartsWith("NzbDrone."))
{
log.Logger = log.Logger.Remove(0, 9);
}
var message = logEvent.FormattedMessage;
if (logEvent.Exception != null)
{
if (string.IsNullOrWhiteSpace(log.Message))
if (string.IsNullOrWhiteSpace(message))
{
log.Message = logEvent.Exception.Message;
message = logEvent.Exception.Message;
}
else
{
log.Message += ": " + logEvent.Exception.Message;
message += ": " + logEvent.Exception.Message;
}
log.Exception = logEvent.Exception.ToString();
log.Exception = CleanseLogMessage.Cleanse(logEvent.Exception.ToString());
log.ExceptionType = logEvent.Exception.GetType().ToString();
}
log.Level = logEvent.Level.Name;
log.Message = CleanseLogMessage.Cleanse(message);
var connectionInfo = _connectionStringFactory.LogDbConnection;

View File

@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Jobs
new ScheduledTask
{
Interval = 6 * 60,
TypeName = typeof(ApplicationCheckUpdateCommand).FullName
TypeName = typeof(ApplicationUpdateCheckCommand).FullName
},
new ScheduledTask

View File

@@ -66,7 +66,7 @@
"UnableToAddANewAppProfilePleaseTryAgain": "غير قادر على إضافة ملف تعريف جودة جديد ، يرجى المحاولة مرة أخرى.",
"UnableToAddANewDownloadClientPleaseTryAgain": "غير قادر على إضافة عميل تنزيل جديد ، يرجى المحاولة مرة أخرى.",
"UnableToAddANewIndexerPleaseTryAgain": "غير قادر على إضافة مفهرس جديد ، يرجى المحاولة مرة أخرى.",
"UnableToLoadBackups": "تعذر تحميل النسخ الاحتياطية",
"BackupsLoadError": "تعذر تحميل النسخ الاحتياطية",
"UnsavedChanges": "التغييرات غير المحفوظة",
"UpdateUiNotWritableHealthCheckMessage": "لا يمكن تثبيت التحديث لأن مجلد واجهة المستخدم '{uiFolder}' غير قابل للكتابة بواسطة المستخدم '{userName}'",
"UpdateScriptPathHelpText": "المسار إلى برنامج نصي مخصص يأخذ حزمة تحديث مستخرجة ويتعامل مع ما تبقى من عملية التحديث",
@@ -329,7 +329,7 @@
"Queued": "في قائمة الانتظار",
"Remove": "إزالة",
"Replace": "يحل محل",
"TheLatestVersionIsAlreadyInstalled": "تم بالفعل تثبيت أحدث إصدار من {0}",
"OnLatestVersion": "تم بالفعل تثبيت أحدث إصدار من {0}",
"DownloadClientPriorityHelpText": "تحديد أولويات عملاء التنزيل المتعددين. يتم استخدام Round-Robin للعملاء الذين لديهم نفس الأولوية.",
"ApplyTagsHelpTextAdd": "إضافة: أضف العلامات إلى قائمة العلامات الموجودة",
"ApplyTagsHelpTextHowToApplyApplications": "كيفية تطبيق العلامات على الأفلام المختارة",
@@ -361,5 +361,14 @@
"Script": "النصي",
"BuiltIn": "مدمج",
"PublishedDate": "تاريخ النشر",
"AllSearchResultsHiddenByFilter": "يتم إخفاء جميع النتائج بواسطة عامل التصفية المطبق"
"AllSearchResultsHiddenByFilter": "يتم إخفاء جميع النتائج بواسطة عامل التصفية المطبق",
"NoEventsFound": "لم يتم العثور على أحداث",
"RestartReloadNote": "ملاحظة: سيتم إعادة تشغيل {appName} تلقائيًا وإعادة تحميل واجهة المستخدم أثناء عملية الاستعادة.",
"UpdateAppDirectlyLoadError": "تعذر تحديث {appName} مباشرة ،",
"DockerUpdater": "تحديث حاوية عامل الإرساء لتلقي التحديث",
"Download": "تحميل",
"ErrorRestoringBackup": "خطأ في استعادة النسخة الاحتياطية",
"ExternalUpdater": "تم تكوين {appName} لاستخدام آلية تحديث خارجية",
"InstallLatest": "تثبيت الأحدث",
"AptUpdater": "استخدم apt لتثبيت التحديث"
}

View File

@@ -153,7 +153,7 @@
"UISettings": "Настройки на потребителския интерфейс",
"UnableToAddANewApplicationPleaseTryAgain": "Не може да се добави ново известие, моля, опитайте отново.",
"UnableToAddANewAppProfilePleaseTryAgain": "Не може да се добави нов качествен профил, моля, опитайте отново.",
"UnableToLoadBackups": "Архивите не могат да се заредят",
"BackupsLoadError": "Архивите не могат да се заредят",
"AllIndexersHiddenDueToFilter": "Всички филми са скрити поради приложен филтър.",
"Level": "Ниво",
"ApplicationStatusCheckAllClientMessage": "Всички списъци са недостъпни поради неуспехи",
@@ -329,7 +329,7 @@
"Queued": "На опашка",
"Remove": "Премахване",
"Replace": "Сменете",
"TheLatestVersionIsAlreadyInstalled": "Вече е инсталирана най-новата версия на {0}",
"OnLatestVersion": "Вече е инсталирана най-новата версия на {0}",
"Genre": "Жанрове",
"ApplyTagsHelpTextRemove": "Премахване: Премахнете въведените тагове",
"ApplyTagsHelpTextHowToApplyIndexers": "Как да приложите тагове към избраните филми",
@@ -361,5 +361,14 @@
"BuiltIn": "Вграден",
"Script": "Сценарий",
"PublishedDate": "Дата на публикуване",
"AllSearchResultsHiddenByFilter": "Всички резултати са скрити от приложения филтър"
"AllSearchResultsHiddenByFilter": "Всички резултати са скрити от приложения филтър",
"DockerUpdater": "актуализирайте контейнера на докера, за да получите актуализацията",
"Download": "Изтегли",
"ErrorRestoringBackup": "Грешка при възстановяване на архивиране",
"ExternalUpdater": "{appName} е конфигуриран да използва външен механизъм за актуализация",
"NoEventsFound": "Няма намерени събития",
"RestartReloadNote": "Забележка: {appName} автоматично ще рестартира и презареди потребителския интерфейс по време на процеса на възстановяване.",
"UpdateAppDirectlyLoadError": "Не може да се актуализира {appName} директно,",
"AptUpdater": "Използвайте apt, за да инсталирате актуализацията",
"InstallLatest": "Инсталирайте най-новите"
}

View File

@@ -43,7 +43,7 @@
"Type": "Tipus",
"UILanguageHelpTextWarning": "Es requereix una recàrrega del navegador",
"UISettings": "Configuració de la interfície",
"UnableToLoadBackups": "No es poden carregar còpies de seguretat",
"BackupsLoadError": "No es poden carregar còpies de seguretat",
"DownloadClientsLoadError": "No es poden carregar els clients de baixada",
"UnableToLoadTags": "No es poden carregar les etiquetes",
"UnableToLoadUISettings": "No es pot carregar la configuració de la IU",
@@ -340,7 +340,7 @@
"UILanguageHelpText": "Idioma que utilitzarà {appName} per a la interfície d'usuari",
"Remove": "Elimina",
"Replace": "Substitueix",
"TheLatestVersionIsAlreadyInstalled": "La darrera versió de {appName} ja està instal·lada",
"OnLatestVersion": "La darrera versió de {appName} ja està instal·lada",
"ThemeHelpText": "Canvieu el tema de la interfície d'usuari de l'aplicació, el tema \"Automàtic\" utilitzarà el tema del vostre sistema operatiu per configurar el mode clar o fosc. Inspirat en {inspiredBy}.",
"ApplicationURL": "URL de l'aplicació",
"Publisher": "Editor",
@@ -485,5 +485,19 @@
"PublishedDate": "Data de publicació",
"Redirected": "Redirecció",
"AllSearchResultsHiddenByFilter": "Tots els resultats estan ocults pel filtre aplicat",
"HealthMessagesInfoBox": "Podeu trobar més informació sobre la causa d'aquests missatges de comprovació de salut fent clic a l'enllaç wiki (icona del llibre) al final de la fila o consultant els vostres [registres]({link}). Si teniu problemes per a interpretar aquests missatges, podeu posar-vos en contacte amb el nostre suport als enllaços següents."
"HealthMessagesInfoBox": "Podeu trobar més informació sobre la causa d'aquests missatges de comprovació de salut fent clic a l'enllaç wiki (icona del llibre) al final de la fila o consultant els vostres [registres]({link}). Si teniu problemes per a interpretar aquests missatges, podeu posar-vos en contacte amb el nostre suport als enllaços següents.",
"AptUpdater": "Utilitzeu apt per a instal·lar l'actualització",
"DockerUpdater": "actualitzeu el contenidor Docker per a rebre l'actualització",
"Download": "Baixa",
"ErrorRestoringBackup": "S'ha produït un error en restaurar la còpia de seguretat",
"ExternalUpdater": "{appName} està configurat per a utilitzar un mecanisme d'actualització extern",
"FailedToFetchUpdates": "No s'han pogut obtenir les actualitzacions",
"LogFilesLocation": "Els fitxers de registre es troben a: {location}",
"Logout": "Tanca la sessió",
"NoEventsFound": "No s'han trobat esdeveniments",
"RestartReloadNote": "Nota: {appName} es reiniciarà i tornarà a carregar automàticament la interfície d'usuari durant el procés de restauració.",
"TheLogLevelDefault": "El nivell de registre per defecte és \"Info\" i es pot canviar a [Configuració general](/configuració/general)",
"UpdateAppDirectlyLoadError": "No es pot actualitzar {appName} directament,",
"WouldYouLikeToRestoreBackup": "Voleu restaurar la còpia de seguretat '{name}'?",
"InstallLatest": "Instal·la l'últim"
}

View File

@@ -198,7 +198,7 @@
"SSLCertPasswordHelpText": "Heslo pro soubor pfx",
"SSLCertPath": "Cesta certifikátu SSL",
"SSLCertPathHelpText": "Cesta k souboru pfx",
"UnableToLoadBackups": "Nelze načíst zálohy",
"BackupsLoadError": "Nelze načíst zálohy",
"DownloadClientsLoadError": "Nelze načíst klienty pro stahování",
"UnableToLoadGeneralSettings": "Nelze načíst obecná nastavení",
"DeleteNotification": "Smazat oznámení",
@@ -329,7 +329,7 @@
"Queued": "Ve frontě",
"Remove": "Odstranit",
"Replace": "Nahradit",
"TheLatestVersionIsAlreadyInstalled": "Nejnovější verze aplikace {appName} je již nainstalována",
"OnLatestVersion": "Nejnovější verze aplikace {appName} je již nainstalována",
"More": "Více",
"ApplyTagsHelpTextAdd": "Přidat: Přidá značky k již existujícímu seznamu",
"ApplyTagsHelpTextHowToApplyApplications": "Jak použít značky na vybrané filmy",
@@ -423,5 +423,15 @@
"BuiltIn": "Vestavěný",
"Script": "Skript",
"PublishedDate": "Datum zveřejnění",
"AllSearchResultsHiddenByFilter": "Všechny výsledky jsou schovány použitým filtrem"
"AllSearchResultsHiddenByFilter": "Všechny výsledky jsou schovány použitým filtrem",
"DockerUpdater": "aktualizujte kontejner dockeru, abyste aktualizaci obdrželi",
"Download": "Stažení",
"ErrorRestoringBackup": "Chyba při obnovování zálohy",
"ExternalUpdater": "{appName} je nakonfigurován pro použití externího aktualizačního mechanismu",
"FailedToFetchUpdates": "Nepodařilo se načíst aktualizace",
"NoEventsFound": "Nebyly nalezeny žádné události",
"RestartReloadNote": "Poznámka: {appName} se během procesu obnovy automaticky restartuje a znovu načte uživatelské rozhraní.",
"UpdateAppDirectlyLoadError": "{appName} nelze aktualizovat přímo,",
"AptUpdater": "K instalaci aktualizace použijte apt",
"InstallLatest": "Nainstalujte nejnovější"
}

View File

@@ -232,7 +232,7 @@
"UnableToAddANewAppProfilePleaseTryAgain": "Kan ikke tilføje en ny kvalitetsprofil, prøv igen.",
"UnableToAddANewDownloadClientPleaseTryAgain": "Kunne ikke tilføje en ny downloadklient. Prøv igen.",
"UnableToAddANewIndexerPleaseTryAgain": "Kunne ikke tilføje en ny indekser. Prøv igen.",
"UnableToLoadBackups": "Kunne ikke indlæse sikkerhedskopier",
"BackupsLoadError": "Kunne ikke indlæse sikkerhedskopier",
"UnableToLoadGeneralSettings": "Kan ikke indlæse generelle indstillinger",
"UnableToLoadNotifications": "Kunne ikke indlæse meddelelser",
"UnableToLoadTags": "Kan ikke indlæse tags",
@@ -340,7 +340,7 @@
"Notification": "Notifikationer",
"Remove": "Fjerne",
"Replace": "erstat",
"TheLatestVersionIsAlreadyInstalled": "Den seneste version af {appName} er allerede installeret",
"OnLatestVersion": "Den seneste version af {appName} er allerede installeret",
"Year": "År",
"ApplyTagsHelpTextAdd": "Tilføj: Føj tags til den eksisterende liste over tags",
"ApplyTagsHelpTextHowToApplyApplications": "Sådan anvendes tags på de valgte film",
@@ -398,5 +398,17 @@
"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",
"NoEventsFound": "Ingen begivenheder fundet",
"AptUpdater": "Brug apt til at installere opdateringen",
"DockerUpdater": "opdater docker-containeren for at modtage opdateringen",
"Download": "Hent",
"ErrorRestoringBackup": "Fejl ved gendannelse af sikkerhedskopi",
"ExternalUpdater": "{appName} er konfigureret til at bruge en ekstern opdateringsmekanisme",
"RestartReloadNote": "Bemærk: {appName} genstarter automatisk og genindlæser brugergrænsefladen under gendannelsesprocessen.",
"UpdateAppDirectlyLoadError": "Kan ikke opdatere {appName} direkte,",
"InstallLatest": "Installer senest"
}

View File

@@ -394,7 +394,7 @@
"TestAllApps": "Alle Apps testen",
"TestAllClients": "Prüfe alle Clients",
"TestAllIndexers": "Prüfe alle Indexer",
"TheLatestVersionIsAlreadyInstalled": "Die aktuellste Version ist bereits installiert",
"OnLatestVersion": "Die aktuellste Version ist bereits installiert",
"ThemeHelpText": "Ändere das UI-Theme der Anwendung. Das 'Auto'-Theme verwendet dein Betriebssystem-Theme, um den hellen oder dunklen Modus einzustellen. Inspiriert von {0}",
"Time": "Zeit",
"Title": "Titel",
@@ -419,7 +419,7 @@
"UnableToAddANewNotificationPleaseTryAgain": "Die neue Benachrichtigung konnte nicht hinzugefügt werden, bitte erneut probieren.",
"UnableToLoadAppProfiles": "App-Profile können nicht geladen werden",
"ApplicationsLoadError": "Anwendungsliste kann nicht geladen werden",
"UnableToLoadBackups": "Sicherungen können nicht geladen werden",
"BackupsLoadError": "Sicherungen können nicht geladen werden",
"UnableToLoadDevelopmentSettings": "Entwicklereinstellungen konnten nicht geladen werden",
"DownloadClientsLoadError": "Downloader konnten nicht geladen werden",
"UnableToLoadGeneralSettings": "Allgemeine Einstellungen konnten nicht geladen werden",
@@ -612,5 +612,17 @@
"BuiltIn": "Eingebaut",
"PublishedDate": "Veröffentlichungsdatum",
"Redirected": "Umleiten",
"AllSearchResultsHiddenByFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet"
"AllSearchResultsHiddenByFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet",
"DockerUpdater": "Aktualisieren Sie den Docker-Container, um das Update zu erhalten",
"Download": "Herunterladen",
"ErrorRestoringBackup": "Fehler beim Wiederherstellen",
"ExternalUpdater": "{appName} wurde so konfiguriert, dass ein externer Update Mechanismus benutzt wird",
"NoEventsFound": "Keine Events gefunden",
"RestartReloadNote": "Hinweis: {appName} startet während des Wiederherstellungsvorgangs automatisch neu und lädt die Benutzeroberfläche neu.",
"TheLogLevelDefault": "Die Protokollebene ist standardmäßig auf „Info“ eingestellt und kann unter „Allgemeine Einstellungen“ (/settings/general) geändert werden.",
"UpdateAppDirectlyLoadError": "{appName} kann nicht direkt aktualisiert werden.",
"UpdaterLogFiles": "Updater-Protokolldateien",
"WouldYouLikeToRestoreBackup": "Willst du das Backup '{name}' wiederherstellen?",
"AptUpdater": "Verwenden Sie apt, um das Update zu installieren",
"InstallLatest": "Jetzt updaten"
}

View File

@@ -234,7 +234,7 @@
"IncludeHealthWarningsHelpText": "Συμπεριλάβετε προειδοποιήσεις για την υγεία",
"Security": "Ασφάλεια",
"Tasks": "Καθήκοντα",
"UnableToLoadBackups": "Δεν είναι δυνατή η φόρτωση αντιγράφων ασφαλείας",
"BackupsLoadError": "Δεν είναι δυνατή η φόρτωση αντιγράφων ασφαλείας",
"DownloadClientsLoadError": "Δεν είναι δυνατή η φόρτωση πελατών λήψης",
"UpdateMechanismHelpText": "Χρησιμοποιήστε το ενσωματωμένο πρόγραμμα ενημέρωσης του {appName} ή ένα script",
"AnalyticsEnabledHelpText": "Στείλτε ανώνυμες πληροφορίες χρήσης και σφάλματος στους διακομιστές του {appName}. Αυτό περιλαμβάνει πληροφορίες στο πρόγραμμα περιήγησής σας, ποιες σελίδες {appName} WebUI χρησιμοποιείτε, αναφορά σφαλμάτων καθώς και έκδοση λειτουργικού συστήματος και χρόνου εκτέλεσης. Θα χρησιμοποιήσουμε αυτές τις πληροφορίες για να δώσουμε προτεραιότητα σε λειτουργίες και διορθώσεις σφαλμάτων.",
@@ -458,7 +458,7 @@
"SyncLevelFull": "Πλήρης συγχρονισμός: Θα διατηρήσει πλήρως συγχρονισμένα τα ευρετήρια αυτής της εφαρμογής. Στη συνέχεια, οι αλλαγές που γίνονται στους indexers στο {appName} συγχρονίζονται με αυτήν την εφαρμογή. Οποιαδήποτε αλλαγή γίνει σε ευρετήρια απομακρυσμένα σε αυτήν την εφαρμογή θα παρακαμφθεί από τον {appName} στον επόμενο συγχρονισμό.",
"Remove": "Αφαιρώ",
"Replace": "Αντικαθιστώ",
"TheLatestVersionIsAlreadyInstalled": "Η τελευταία έκδοση του {appName} είναι ήδη εγκατεστημένη",
"OnLatestVersion": "Η τελευταία έκδοση του {appName} είναι ήδη εγκατεστημένη",
"ApiKeyValidationHealthCheckMessage": "Παρακαλούμε ενημερώστε το κλείδι API ώστε να έχει τουλάχιστον {length} χαρακτήρες. Μπορείτε να το κάνετε αυτό μέσα από τις ρυθμίσεις ή το αρχείο ρυθμίσεων",
"StopSelecting": "Διακοπή Επιλογής",
"OnHealthRestored": "Στην Αποκατάσταση Υγείας",
@@ -529,5 +529,14 @@
"BuiltIn": "Ενσωματωμένο",
"PublishedDate": "Ημερομηνία δημοσίευσης",
"Redirected": "Διευθύνω πάλιν",
"AllSearchResultsHiddenByFilter": "Όλα τα αποτελέσματα αποκρύπτονται από το εφαρμοσμένο φίλτρο"
"AllSearchResultsHiddenByFilter": "Όλα τα αποτελέσματα αποκρύπτονται από το εφαρμοσμένο φίλτρο",
"Download": "Κατεβάστε",
"ErrorRestoringBackup": "Σφάλμα κατά την επαναφορά του αντιγράφου ασφαλείας",
"ExternalUpdater": "Το {appName} έχει ρυθμιστεί να χρησιμοποιεί έναν εξωτερικό μηχανισμό ενημέρωσης",
"NoEventsFound": "Δεν βρέθηκαν συμβάντα",
"RestartReloadNote": "Σημείωση: Το {appName} θα επανεκκινήσει αυτόματα και θα φορτώσει ξανά το περιβάλλον εργασίας χρήστη κατά τη διαδικασία επαναφοράς.",
"UpdateAppDirectlyLoadError": "Δεν είναι δυνατή η απευθείας ενημέρωση του {appName},",
"DockerUpdater": "ενημερώστε το κοντέινερ για να λάβετε την ενημέρωση",
"AptUpdater": "Χρησιμοποιήστε το apt για να εγκαταστήσετε την ενημέρωση",
"InstallLatest": "Εγκατάσταση πιο πρόσφατου"
}

View File

@@ -68,6 +68,7 @@
"Apps": "Apps",
"AppsMinimumSeeders": "Apps Minimum Seeders",
"AppsMinimumSeedersHelpText": "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default",
"AptUpdater": "Use apt to install the update",
"AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?",
"AreYouSureYouWantToDeleteIndexer": "Are you sure you want to delete '{name}' from {appName}?",
"Artist": "Artist",
@@ -98,6 +99,7 @@
"BackupNow": "Backup Now",
"BackupRetentionHelpText": "Automatic backups older than the retention period will be cleaned up automatically",
"Backups": "Backups",
"BackupsLoadError": "Unable to load backups",
"BasicSearch": "Basic Search",
"BeforeUpdate": "Before update",
"BindAddress": "Bind Address",
@@ -184,8 +186,10 @@
"DisabledUntil": "Disabled Until",
"Discord": "Discord",
"Docker": "Docker",
"DockerUpdater": "Update the docker container to receive the update",
"Donate": "Donate",
"Donations": "Donations",
"Download": "Download",
"DownloadClient": "Download Client",
"DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location",
"DownloadClientCategory": "Download Client Category",
@@ -269,12 +273,15 @@
"Episode": "Episode",
"Error": "Error",
"ErrorLoadingContents": "Error loading contents",
"ErrorRestoringBackup": "Error restoring backup",
"EventType": "Event Type",
"Events": "Events",
"Exception": "Exception",
"ExistingTag": "Existing tag",
"External": "External",
"ExternalUpdater": "{appName} is configured to use an external update mechanism",
"Failed": "Failed",
"FailedToFetchUpdates": "Failed to fetch updates",
"FeatureRequests": "Feature Requests",
"Filename": "Filename",
"Files": "Files",
@@ -320,6 +327,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 +392,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 +421,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",
@@ -429,6 +445,11 @@
"Info": "Info",
"InfoUrl": "Info URL",
"InitialFailure": "Initial Failure",
"Install": "Install",
"InstallLatest": "Install Latest",
"InstallMajorVersionUpdate": "Install Update",
"InstallMajorVersionUpdateMessage": "This update will install a new major version and may not be compatible with your system. Are you sure you want to install this update?",
"InstallMajorVersionUpdateMessageLink": "Please check [{domain}]({url}) for more information.",
"InstanceName": "Instance Name",
"InstanceNameHelpText": "Instance name in tab and for Syslog app name",
"InteractiveSearch": "Interactive Search",
@@ -446,11 +467,13 @@
"Level": "Level",
"Link": "Link",
"LogFiles": "Log Files",
"LogFilesLocation": "Log files are located in: {location}",
"LogLevel": "Log Level",
"LogLevelTraceHelpTextWarning": "Trace logging should only be enabled temporarily",
"LogSizeLimit": "Log Size Limit",
"LogSizeLimitHelpText": "Maximum log file size in MB before archiving. Default is 1MB.",
"Logging": "Logging",
"Logout": "Logout",
"Logs": "Logs",
"MIA": "MIA",
"MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details",
@@ -487,6 +510,7 @@
"NoChange": "No Change",
"NoChanges": "No Changes",
"NoDownloadClientsFound": "No download clients found",
"NoEventsFound": "No events found",
"NoHistoryFound": "No history found",
"NoIndexerCategories": "No categories found for this indexer",
"NoIndexerHistory": "No history found for this indexer",
@@ -520,6 +544,7 @@
"OnHealthIssueHelpText": "On Health Issue",
"OnHealthRestored": "On Health Restored",
"OnHealthRestoredHelpText": "On Health Restored",
"OnLatestVersion": "The latest version of {appName} is already installed",
"Open": "Open",
"OpenBrowserOnStart": "Open browser on start",
"OpenThisModal": "Open This Modal",
@@ -541,6 +566,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}",
@@ -595,6 +622,7 @@
"Restart": "Restart",
"RestartNow": "Restart Now",
"RestartProwlarr": "Restart {appName}",
"RestartReloadNote": "Note: {appName} will automatically restart and reload the UI during the restore process.",
"RestartRequiredHelpTextWarning": "Requires restart to take effect",
"Restore": "Restore",
"RestoreBackup": "Restore Backup",
@@ -692,7 +720,7 @@
"TestAllApps": "Test All Apps",
"TestAllClients": "Test All Clients",
"TestAllIndexers": "Test All Indexers",
"TheLatestVersionIsAlreadyInstalled": "The latest version of {appName} is already installed",
"TheLogLevelDefault": "The log level defaults to 'Info' and can be changed in [General Settings](/settings/general)",
"Theme": "Theme",
"ThemeHelpText": "Change Application UI Theme, 'Auto' Theme will use your OS Theme to set Light or Dark mode. Inspired by {inspiredBy}.",
"Time": "Time",
@@ -732,7 +760,6 @@
"UnableToAddANewIndexerProxyPleaseTryAgain": "Unable to add a new indexer proxy, please try again.",
"UnableToAddANewNotificationPleaseTryAgain": "Unable to add a new notification, please try again.",
"UnableToLoadAppProfiles": "Unable to load app profiles",
"UnableToLoadBackups": "Unable to load backups",
"UnableToLoadDevelopmentSettings": "Unable to load Development settings",
"UnableToLoadGeneralSettings": "Unable to load General settings",
"UnableToLoadHistory": "Unable to load history",
@@ -743,6 +770,7 @@
"UnableToLoadUISettings": "Unable to load UI settings",
"UnsavedChanges": "Unsaved Changes",
"UnselectAll": "Unselect All",
"UpdateAppDirectlyLoadError": "Unable to update {appName} directly,",
"UpdateAutomaticallyHelpText": "Automatically download and install updates. You will still be able to install from System: Updates",
"UpdateAvailableHealthCheckMessage": "New update is available: {version}",
"UpdateMechanismHelpText": "Use {appName}'s built-in updater or a script",
@@ -750,6 +778,7 @@
"UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is not writable by the user '{userName}'.",
"UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is in an App Translocation folder.",
"UpdateUiNotWritableHealthCheckMessage": "Cannot install update because UI folder '{uiFolder}' is not writable by the user '{userName}'.",
"UpdaterLogFiles": "Updater Log Files",
"Updates": "Updates",
"Uptime": "Uptime",
"Url": "Url",
@@ -767,6 +796,7 @@
"Website": "Website",
"WhatsNew": "What's New?",
"Wiki": "Wiki",
"WouldYouLikeToRestoreBackup": "Would you like to restore the backup '{name}'?",
"XmlRpcPath": "XML RPC Path",
"Year": "Year",
"Yes": "Yes",

View File

@@ -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",
@@ -248,7 +248,7 @@
"UnableToLoadUISettings": "No se han podido cargar los ajustes de UI",
"UnableToLoadHistory": "No se ha podido cargar la historia",
"UnableToLoadGeneralSettings": "No se han podido cargar los ajustes Generales",
"UnableToLoadBackups": "No se pudo cargar las copias de seguridad",
"BackupsLoadError": "No se pudo cargar las copias de seguridad",
"UnableToAddANewNotificationPleaseTryAgain": "No se ha podido añadir una nueva notificación, prueba otra vez.",
"UnableToAddANewIndexerPleaseTryAgain": "No se pudo añadir un nuevo indexador, por favor inténtalo de nuevo.",
"UnableToAddANewDownloadClientPleaseTryAgain": "No se ha podido añadir un nuevo gestor de descargas, prueba otra vez.",
@@ -363,7 +363,7 @@
"Started": "Iniciado",
"Remove": "Eliminar",
"Replace": "Reemplazar",
"TheLatestVersionIsAlreadyInstalled": "La última versión de {appName} ya está instalada",
"OnLatestVersion": "La última versión de {appName} ya está instalada",
"Apps": "Aplicaciones",
"AddApplication": "Añadir aplicación",
"AddCustomFilter": "Añadir Filtro Personalizado",
@@ -771,7 +771,37 @@
"AverageGrabs": "Promedio de capturas",
"AllSearchResultsHiddenByFilter": "Todos los resultados están ocultos por el filtro aplicado.",
"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.",
"HealthMessagesInfoBox": "Puedes 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 tus [registros]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro soporte 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",
"LogFilesLocation": "Los archivos de registro se encuentran en: {location}",
"DockerUpdater": "Actualiza el contenedor docker para recibir la actualización",
"Download": "Descargar",
"ErrorRestoringBackup": "Error restaurando la copia de seguridad",
"ExternalUpdater": "{appName} está configurado para usar un mecanismo de actualización externo",
"FailedToFetchUpdates": "Fallo al buscar las actualizaciones",
"Logout": "Cerrar Sesión",
"NoEventsFound": "Ningún evento encontrado",
"RestartReloadNote": "Nota: {appName} se reiniciará automáticamente y recargará la interfaz durante el proceso de restauración.",
"TheLogLevelDefault": "El nivel de registro por defecto es 'Info' y puede ser cambiado en [Opciones generales](opciones/general)",
"UpdateAppDirectlyLoadError": "No se pudo actualizar {appName} directamente,",
"UpdaterLogFiles": "Actualizador de archivos de registro",
"WouldYouLikeToRestoreBackup": "Te gustaria restaurar la copia de seguridad '{name}'?",
"AptUpdater": "Use apt para instalar la actualización",
"Install": "Instalar",
"InstallLatest": "Instala el último",
"InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?",
"InstallMajorVersionUpdate": "Instalar actualización",
"InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información."
}

View File

@@ -133,7 +133,7 @@
"UnableToAddANewApplicationPleaseTryAgain": "Uuden sovelluksen lisäys epäonnistui. Yritä uudelleen.",
"UnableToAddANewIndexerPleaseTryAgain": "Uuden tietolähteen lisäys epäonnistui. Yritä uudelleen.",
"UnableToAddANewIndexerProxyPleaseTryAgain": "Uuden tiedonhaun välityspalvelimen lisäys epäonnistui. Yritä uudelleen.",
"UnableToLoadBackups": "Varmuuskopioiden lataus epäonnistui",
"BackupsLoadError": "Varmuuskopioiden lataus epäonnistui",
"DownloadClientsLoadError": "Lataustyökalujen lataus ei onistu",
"UnableToLoadGeneralSettings": "Virhe ladattaessa yleisiä asetuksia",
"UpdateAutomaticallyHelpText": "Lataa ja asenna päivitykset automaattisesti. Voit myös edelleen suorittaa asennuksen järjestelmäasetusten päivitykset-osiosta.",
@@ -455,7 +455,7 @@
"AuthenticationRequired": "Vaadi tunnistautuminen",
"Remove": "Poista",
"Replace": "Korvaa",
"TheLatestVersionIsAlreadyInstalled": "{appName}in uusin versio on jo asennettu",
"OnLatestVersion": "{appName}in uusin versio on jo asennettu",
"ApplicationURL": "Sovelluksen URL",
"ApplicationUrlHelpText": "Tämän sovelluksen ulkoinen URL-osoite, johon sisältyy http(s)://, portti ja URL-perusta.",
"Track": "Valvo",
@@ -699,5 +699,20 @@
"Redirected": "Uudelleenohjaus",
"AllSearchResultsHiddenByFilter": "Aktiivinen suodatin piilottaa kaikki tulokset.",
"HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli kohtaat ongelmia näiden viestien tulkinnassa, tavoitat tukemme alla olevilla linkkeillä.",
"PackageVersionInfo": "{packageVersion} julkaisijalta {packageAuthor}"
"PackageVersionInfo": "{packageVersion} julkaisijalta {packageAuthor}",
"ErrorRestoringBackup": "Virhe palautettaessa varmuuskopiota",
"ExternalUpdater": "{appName} on määritetty käyttämään ulkoista päivitysratkaisua.",
"FailedToFetchUpdates": "Päivitysten nouto epäonnistui",
"AptUpdater": "Asenna päivitys APT-työkalun avulla",
"DockerUpdater": "Hanki päivitys päivittämällä Docker-säiliö",
"Download": "Lataa",
"LogFilesLocation": "Lokitiedostojen tallennussijainti: {location}",
"Logout": "Kirjaudu ulos",
"NoEventsFound": "Tapahtumia ei löytynyt",
"RestartReloadNote": "Huomioi: {appName} käynnistyy palautusprosessin aikana automaattisesti uudelleen.",
"TheLogLevelDefault": "Lokikirjauksen oletusarvoinen laajuus on \"Informatiivinen\". Laajuutta voidaan muuttaa [Yleisistä asetuksista](/settings/general).",
"UpdateAppDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,",
"UpdaterLogFiles": "Päivittäjän lokitiedostot",
"WouldYouLikeToRestoreBackup": "Haluatko palauttaa varmuuskopion \"{name}\"?",
"InstallLatest": "Asenna uusin"
}

View File

@@ -214,7 +214,7 @@
"UnableToLoadHistory": "Impossible de charger l'historique",
"UnableToLoadGeneralSettings": "Impossible de charger les paramètres généraux",
"DownloadClientsLoadError": "Impossible de charger les clients de téléchargement",
"UnableToLoadBackups": "Impossible de charger les sauvegardes",
"BackupsLoadError": "Impossible de charger les sauvegardes",
"UnableToAddANewNotificationPleaseTryAgain": "Impossible d'ajouter une nouvelle notification, veuillez réessayer.",
"UnableToAddANewIndexerPleaseTryAgain": "Impossible d'ajouter un nouvel indexeur, veuillez réessayer.",
"UnableToAddANewDownloadClientPleaseTryAgain": "Impossible d'ajouter un nouveau client de téléchargement, veuillez réessayer.",
@@ -458,7 +458,7 @@
"AuthenticationRequiredWarning": "Pour empêcher l'accès à distance sans authentification, {appName} exige désormais que l'authentification soit activée. Vous pouvez éventuellement désactiver l'authentification pour les adresses locales.",
"Remove": "Retirer",
"Replace": "Remplacer",
"TheLatestVersionIsAlreadyInstalled": "La dernière version de {appName} est déjà installée",
"OnLatestVersion": "La dernière version de {appName} est déjà installée",
"AddCustomFilter": "Ajouter filtre personnalisé",
"AddApplication": "Ajouter une application",
"IncludeManualGrabsHelpText": "Inclure les saisies manuelles effectuées dans {appName}",
@@ -773,5 +773,26 @@
"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",
"DockerUpdater": "Mettez à jour le conteneur Docker pour recevoir la mise à jour",
"Download": "Téléchargement",
"ErrorRestoringBackup": "Erreur lors de la restauration de la sauvegarde",
"ExternalUpdater": "{appName} est configuré pour utiliser un mécanisme de mise à jour externe",
"FailedToFetchUpdates": "Échec de la récupération des mises à jour",
"LogFilesLocation": "Les fichiers journaux sont situés dans : {location}",
"Logout": "Se déconnecter",
"NoEventsFound": "Aucun événement trouvé",
"RestartReloadNote": "Remarque : {appName} redémarrera et rechargera automatiquement l'interface utilisateur pendant le processus de restauration.",
"TheLogLevelDefault": "Le niveau de journalisation est par défaut à « Information » et peut être modifié dans les [paramètres généraux](/settings/general)",
"UpdateAppDirectlyLoadError": "Impossible de mettre à jour directement {appName},",
"UpdaterLogFiles": "Journaux du programme de mise à jour",
"WouldYouLikeToRestoreBackup": "Souhaitez-vous restaurer la sauvegarde « {name} » ?",
"AptUpdater": "Utiliser apt pour installer la mise à jour",
"Install": "Installer",
"InstallLatest": "Installer la dernière",
"InstallMajorVersionUpdateMessageLink": "Veuillez consulter [{domain}]({url}) pour plus d'informations.",
"InstallMajorVersionUpdate": "Installer la mise à jour",
"InstallMajorVersionUpdateMessage": "Cette mise à jour installera une nouvelle version majeure et pourrait ne pas être compatible avec votre système. Êtes-vous sûr de vouloir installer cette mise à jour ?"
}

View File

@@ -68,7 +68,7 @@
"UILanguageHelpTextWarning": "חובה לטעון דפדפן",
"UISettings": "הגדרות ממשק המשתמש",
"UnableToAddANewAppProfilePleaseTryAgain": "לא ניתן להוסיף פרופיל איכות חדש, נסה שוב.",
"UnableToLoadBackups": "לא ניתן לטעון גיבויים",
"BackupsLoadError": "לא ניתן לטעון גיבויים",
"UnableToLoadTags": "לא ניתן לטעון תגים",
"UnableToLoadUISettings": "לא ניתן לטעון הגדרות ממשק משתמש",
"UnsavedChanges": "שינויים שלא נשמרו",
@@ -371,7 +371,7 @@
"EditSyncProfile": "הוספת פרופיל סינכרון",
"Notifications": "התראות",
"Notification": "התראות",
"TheLatestVersionIsAlreadyInstalled": "הגרסה האחרונה של {appName} כבר מותקנת",
"OnLatestVersion": "הגרסה האחרונה של {appName} כבר מותקנת",
"Remove": "לְהַסִיר",
"Replace": "החלף",
"AddApplication": "הוספת אפליקציה",
@@ -418,5 +418,14 @@
"AddCategory": "הוסף קטגוריה",
"ActiveApps": "אפליקציות פעילות",
"ActiveIndexers": "אינדקסרים פעילים",
"AllSearchResultsHiddenByFilter": "כל התוצאות מוסתרות על ידי המסנן שהוחל"
"AllSearchResultsHiddenByFilter": "כל התוצאות מוסתרות על ידי המסנן שהוחל",
"InstallLatest": "התקן את האחרונה",
"NoEventsFound": "לא נמצאו אירועים",
"DockerUpdater": "עדכן את מיכל העגינה לקבל את העדכון",
"Download": "הורד",
"ErrorRestoringBackup": "שגיאה בשחזור הגיבוי",
"ExternalUpdater": "{appName} מוגדר להשתמש במנגנון עדכון חיצוני",
"RestartReloadNote": "הערה: {appName} יופעל מחדש אוטומטית וטען מחדש את ממשק המשתמש במהלך תהליך השחזור.",
"UpdateAppDirectlyLoadError": "לא ניתן לעדכן את {appName} ישירות,",
"AptUpdater": "השתמש ב- apt כדי להתקין את העדכון"
}

View File

@@ -111,7 +111,7 @@
"UnableToAddANewDownloadClientPleaseTryAgain": "नया डाउनलोड क्लाइंट जोड़ने में असमर्थ, कृपया पुनः प्रयास करें।",
"UnableToAddANewIndexerPleaseTryAgain": "नया अनुक्रमणिका जोड़ने में असमर्थ, कृपया पुनः प्रयास करें।",
"UnableToAddANewIndexerProxyPleaseTryAgain": "नया अनुक्रमणिका जोड़ने में असमर्थ, कृपया पुनः प्रयास करें।",
"UnableToLoadBackups": "बैकअप लोड करने में असमर्थ",
"BackupsLoadError": "बैकअप लोड करने में असमर्थ",
"NoTagsHaveBeenAddedYet": "अभी तक कोई टैग नहीं जोड़े गए हैं",
"Reddit": "reddit",
"UpdateMechanismHelpText": "रेडर के बिल्ट इन अपडेटर या स्क्रिप्ट का उपयोग करें",
@@ -328,7 +328,7 @@
"LastExecution": "अंतिम निष्पादन",
"Queued": "कतारबद्ध",
"Remove": "हटाना",
"TheLatestVersionIsAlreadyInstalled": "रेडर का नवीनतम संस्करण पहले से ही स्थापित है",
"OnLatestVersion": "रेडर का नवीनतम संस्करण पहले से ही स्थापित है",
"Replace": "बदलने के",
"More": "अधिक",
"DeleteSelectedDownloadClients": "डाउनलोड क्लाइंट हटाएं",
@@ -360,5 +360,13 @@
"BuiltIn": "में निर्मित",
"Script": "लिपि",
"PublishedDate": "प्रकाशित तिथि",
"AllSearchResultsHiddenByFilter": "सभी परिणाम लागू फ़िल्टर द्वारा छिपे हुए हैं"
"AllSearchResultsHiddenByFilter": "सभी परिणाम लागू फ़िल्टर द्वारा छिपे हुए हैं",
"AptUpdater": "अद्यतन स्थापित करने के लिए उपयुक्त का उपयोग करें",
"DockerUpdater": "अपडेट प्राप्त करने के लिए docker कंटेनर को अपडेट करें",
"Download": "डाउनलोड",
"ErrorRestoringBackup": "बैकअप बहाल करने में त्रुटि",
"NoEventsFound": "कोई घटना नहीं मिली",
"RestartReloadNote": "नोट: रैडियर स्वचालित रूप से पुनः आरंभ करेगा और पुनर्स्थापना प्रक्रिया के दौरान UI को फिर से लोड करेगा।",
"UpdateAppDirectlyLoadError": "सीधे {appName} अद्यतन करने में असमर्थ,",
"InstallLatest": "नवीनतम स्थापित करें"
}

View File

@@ -181,5 +181,35 @@
"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.",
"EditApplicationImplementation": "Dodaj Vezu - {implementationName}",
"AptUpdater": "Koristi apt kako bi instalirao ažuriranje",
"EditIndexerProxyImplementation": "Dodaj Indexer - {implementationName}"
}

View File

@@ -224,7 +224,7 @@
"UnableToLoadHistory": "Nem sikerült betölteni az előzményeket",
"UnableToLoadGeneralSettings": "Nem sikerült betölteni az általános beállításokat",
"DownloadClientsLoadError": "Nem sikerült betölteni a letöltőkliens(eke)t",
"UnableToLoadBackups": "Biztonsági mentés(ek) betöltése sikertelen",
"BackupsLoadError": "Biztonsági mentés(ek) betöltése sikertelen",
"UnableToAddANewNotificationPleaseTryAgain": "Nem lehet új értesítést hozzáadni, próbálkozz újra.",
"UnableToAddANewIndexerPleaseTryAgain": "Nem lehet új indexert hozzáadni, próbálkozz újra.",
"UnableToAddANewDownloadClientPleaseTryAgain": "Nem lehet új letöltőklienst hozzáadni, próbálkozz újra.",
@@ -456,7 +456,7 @@
"AuthenticationRequired": "Azonosítás szükséges",
"AuthenticationRequiredHelpText": "Módosítsa, hogy mely kérésekhez van szükség hitelesítésre. Ne változtasson, hacsak nem érti a kockázatokat.",
"AuthenticationRequiredWarning": "A hitelesítés nélküli távoli hozzáférés megakadályozása érdekében a(z) {appName} alkalmazásnak engedélyeznie kell a hitelesítést. Opcionálisan letilthatja a helyi címekről történő hitelesítést.",
"TheLatestVersionIsAlreadyInstalled": "A {appName} legújabb verziója már telepítva van",
"OnLatestVersion": "A {appName} legújabb verziója már telepítva van",
"Remove": "Eltávolítás",
"Replace": "Kicserél",
"ApplicationURL": "Alkalmazás URL",
@@ -582,5 +582,18 @@
"PublishedDate": "Közzététel dátuma",
"Redirected": "Átirányítás",
"AllSearchResultsHiddenByFilter": "Az alkalmazott szűrők miatt, az összes keresési eredmény rejtve marad",
"HealthMessagesInfoBox": "Az állapotfelmérés okáról további információkat találhat, ha a sor végén található wikilinkre (könyv ikonra) kattint, vagy megnézi [logs] ({link}). Ha nehézségei vannak ezen üzenetek értelmezése során, forduljon ügyfélszolgálatunkhoz az alábbi linkeken."
"HealthMessagesInfoBox": "Az állapotfelmérés okáról további információkat találhat, ha a sor végén található wikilinkre (könyv ikonra) kattint, vagy megnézi [logs] ({link}). Ha nehézségei vannak ezen üzenetek értelmezése során, forduljon ügyfélszolgálatunkhoz az alábbi linkeken.",
"AptUpdater": "A frissítés telepítéséhez használja az apt-t",
"DockerUpdater": "Frissítse a docker-tárolót a frissítés fogadásához",
"Download": "Letöltés",
"ErrorRestoringBackup": "Hiba a biztonsági mentés visszaállításakor",
"ExternalUpdater": "A {appName} egy külső frissítési mechanizmus használatára van konfigurálva",
"FailedToFetchUpdates": "Nem sikerült lekérni a frissítéseket",
"LogFilesLocation": "A naplófájlok itt találhatók: {location}",
"Logout": "Kijelentkezés",
"NoEventsFound": "Nem található események",
"RestartReloadNote": "Megjegyzés: A {appName} automatikusan újraindítja és újratölti a felületet a visszaállítási folyamatban.",
"UpdateAppDirectlyLoadError": "Nem lehetséges közvetlenül frissíteni a {appName}-t",
"WouldYouLikeToRestoreBackup": "Szeretné visszaállítani a(z) „{name}” biztonsági másolatot?",
"InstallLatest": "Legfrissebb telepítése"
}

View File

@@ -85,5 +85,6 @@
"Id": "ID",
"IndexerHDBitsSettingsCodecs": "Codec",
"ProxyValidationBadRequest": "Gagal menguji proxy. Kode Status: {statusCode}",
"AllSearchResultsHiddenByFilter": "Seluruh hasil disembunyikan karena penyaringan yang diterapkan"
"AllSearchResultsHiddenByFilter": "Seluruh hasil disembunyikan karena penyaringan yang diterapkan",
"AptUpdater": "Gunakan apt untuk memasang pembaruan"
}

View File

@@ -64,7 +64,7 @@
"Torrents": "Flæði",
"Type": "Tegund",
"UnableToAddANewApplicationPleaseTryAgain": "Ekki er hægt að bæta við nýrri tilkynningu. Reyndu aftur.",
"UnableToLoadBackups": "Ekki er hægt að hlaða afrit",
"BackupsLoadError": "Ekki er hægt að hlaða afrit",
"DownloadClientsLoadError": "Ekki er hægt að hlaða niður viðskiptavinum",
"UnableToLoadGeneralSettings": "Ekki er hægt að hlaða almennar stillingar",
"UnableToLoadHistory": "Ekki er hægt að hlaða sögu",
@@ -329,7 +329,7 @@
"NextExecution": "Næsta framkvæmd",
"Remove": "Fjarlægðu",
"Replace": "Skipta um",
"TheLatestVersionIsAlreadyInstalled": "Nýjasta útgáfan af {appName} er þegar uppsett",
"OnLatestVersion": "Nýjasta útgáfan af {appName} er þegar uppsett",
"ApplyTagsHelpTextAdd": "Bæta við: Bættu merkjum við núverandi lista yfir merki",
"ApplyTagsHelpTextHowToApplyApplications": "Hvernig á að setja merki á völdu kvikmyndirnar",
"ApplyTagsHelpTextHowToApplyIndexers": "Hvernig á að setja merki á völdu kvikmyndirnar",
@@ -361,5 +361,14 @@
"BuiltIn": "Innbyggð",
"Script": "Handrit",
"PublishedDate": "Útgáfudagur",
"AllSearchResultsHiddenByFilter": "Allar niðurstöður eru faldar af beittu síunni"
"AllSearchResultsHiddenByFilter": "Allar niðurstöður eru faldar af beittu síunni",
"AptUpdater": "Notaðu apt til að setja uppfærsluna upp",
"DockerUpdater": "uppfærðu bryggjugáminn til að fá uppfærsluna",
"Download": "Sækja",
"ErrorRestoringBackup": "Villa við að endurheimta afrit",
"ExternalUpdater": "{appName} er stilltur til að nota ytri uppfærslu",
"RestartReloadNote": "Athugið: {appName} mun sjálfkrafa endurræsa og endurhlaða notendaviðmiðið meðan á endurreisnarferlinu stendur.",
"UpdateAppDirectlyLoadError": "Ekki er hægt að uppfæra {appName} beint,",
"NoEventsFound": "Engir viðburðir fundust",
"InstallLatest": "Settu upp nýjustu"
}

View File

@@ -240,7 +240,7 @@
"UnableToLoadHistory": "Impossibile caricare la storia",
"UnableToLoadGeneralSettings": "Impossibile caricare le impostazioni Generali",
"DownloadClientsLoadError": "Impossibile caricare i client di download",
"UnableToLoadBackups": "Impossibile caricare i backup",
"BackupsLoadError": "Impossibile caricare i backup",
"UnableToAddANewNotificationPleaseTryAgain": "Impossibile aggiungere una nuova notifica, riprova.",
"UnableToAddANewIndexerPleaseTryAgain": "Impossibile aggiungere un nuovo Indicizzatore, riprova.",
"UnableToAddANewDownloadClientPleaseTryAgain": "Impossibile aggiungere un nuovo client di download, riprova.",
@@ -456,7 +456,7 @@
"MappedCategories": "Categorie mappate",
"Remove": "Rimuovi",
"Replace": "Sostituire",
"TheLatestVersionIsAlreadyInstalled": "L'ultima versione di {appName} è già installata",
"OnLatestVersion": "L'ultima versione di {appName} è già installata",
"ApplicationURL": "URL Applicazione",
"ApplicationUrlHelpText": "L'URL esterno di questa applicazione, incluso http(s)://, porta e URL base",
"Episode": "Episodio",
@@ -647,5 +647,16 @@
"PublishedDate": "Data Pubblicazione",
"Redirected": "Reindirizzamento",
"AllSearchResultsHiddenByFilter": "Tutti i risultati sono nascosti dal filtro",
"PackageVersionInfo": "{packageVersion} di {packageAuthor}"
"PackageVersionInfo": "{packageVersion} di {packageAuthor}",
"DockerUpdater": "Aggiorna il container di docker per ricevere l'aggiornamento",
"Download": "Scarica",
"ErrorRestoringBackup": "Errore durante il ripristino del backup",
"ExternalUpdater": "{appName} è configurato per utilizzare un meccanismo di aggiornamento esterno",
"LogFilesLocation": "File di Log localizzati in: {location}",
"NoEventsFound": "Nessun evento trovato",
"RestartReloadNote": "Nota: {appName} si riavvierà automaticamente e ricaricherà l'interfaccia durante il processo di ripristino.",
"WouldYouLikeToRestoreBackup": "Vuoi ripristinare il backup '{name}'?",
"UpdateAppDirectlyLoadError": "Impossibile aggiornare {appName} direttamente,",
"AptUpdater": "Usa apt per installare l'aggiornamento",
"InstallLatest": "Installa il più recente"
}

View File

@@ -206,7 +206,7 @@
"UnableToAddANewDownloadClientPleaseTryAgain": "新しいダウンロードクライアントを追加できません。もう一度やり直してください。",
"UnableToAddANewIndexerPleaseTryAgain": "新しいインデクサーを追加できません。もう一度やり直してください。",
"UnableToAddANewNotificationPleaseTryAgain": "新しい通知を追加できません。もう一度やり直してください。",
"UnableToLoadBackups": "バックアップを読み込めません",
"BackupsLoadError": "バックアップを読み込めません",
"UnableToLoadHistory": "履歴を読み込めません",
"UnableToLoadTags": "タグを読み込めません",
"UnableToLoadUISettings": "UI設定を読み込めません",
@@ -329,7 +329,7 @@
"Queued": "キューに入れられました",
"Remove": "削除する",
"Replace": "交換",
"TheLatestVersionIsAlreadyInstalled": "{appName}の最新バージョンはすでにインストールされています",
"OnLatestVersion": "{appName}の最新バージョンはすでにインストールされています",
"Track": "痕跡",
"DeleteSelectedDownloadClients": "ダウンロードクライアントを削除する",
"Genre": "ジャンル",
@@ -361,5 +361,14 @@
"Script": "脚本",
"BuiltIn": "ビルトイン",
"PublishedDate": "公開日",
"AllSearchResultsHiddenByFilter": "すべての結果は、適用されたフィルターによって非表示になります"
"AllSearchResultsHiddenByFilter": "すべての結果は、適用されたフィルターによって非表示になります",
"DockerUpdater": "Dockerコンテナを更新して、更新を受信します",
"Download": "ダウンロード",
"ErrorRestoringBackup": "バックアップの復元中にエラーが発生しました",
"ExternalUpdater": "{appName}は、外部更新メカニズムを使用するように構成されています",
"NoEventsFound": "イベントが見つかりません",
"RestartReloadNote": "注:{appName}は、復元プロセス中にUIを自動的に再起動して再読み込みします。",
"UpdateAppDirectlyLoadError": "{appName}を直接更新できません。",
"AptUpdater": "aptを使用してアップデートをインストールします",
"InstallLatest": "最新のインストール"
}

View File

@@ -85,7 +85,7 @@
"UnableToAddANewAppProfilePleaseTryAgain": "새 품질 프로필을 추가 할 수 없습니다. 다시 시도하십시오.",
"UnableToAddANewDownloadClientPleaseTryAgain": "새 다운로드 클라이언트를 추가 할 수 없습니다. 다시 시도하십시오.",
"UnableToAddANewIndexerPleaseTryAgain": "새 인덱서를 추가 할 수 없습니다. 다시 시도하십시오.",
"UnableToLoadBackups": "백업을로드 할 수 없습니다.",
"BackupsLoadError": "백업을로드 할 수 없습니다.",
"UpdateAutomaticallyHelpText": "업데이트를 자동으로 다운로드하고 설치합니다. 시스템 : 업데이트에서 계속 설치할 수 있습니다.",
"RemoveFilter": "필터 제거",
"Size": "크기",
@@ -328,7 +328,7 @@
"LastExecution": "마지막 실행",
"Queued": "대기 중",
"Replace": "바꾸다",
"TheLatestVersionIsAlreadyInstalled": "최신 버전의 Whisparr가 이미 설치되어 있습니다.",
"OnLatestVersion": "최신 버전의 Whisparr가 이미 설치되어 있습니다.",
"Remove": "없애다",
"Genre": "장르",
"ApplyTagsHelpTextAdd": "추가 : 기존 태그 목록에 태그를 추가합니다.",
@@ -360,5 +360,12 @@
"ProxyValidationBadRequest": "프록시를 테스트하지 못했습니다. StatusCode : {statusCode}",
"BuiltIn": "내장",
"PublishedDate": "발행일",
"AllSearchResultsHiddenByFilter": "적용된 필터에 의해 모든 결과가 숨겨집니다."
"AllSearchResultsHiddenByFilter": "적용된 필터에 의해 모든 결과가 숨겨집니다.",
"DockerUpdater": "Docker 컨테이너를 업데이트하여 업데이트를 받으십시오.",
"Download": "다운로드",
"ErrorRestoringBackup": "백업 복원 오류",
"ExternalUpdater": "{appName}는 외부 업데이트 메커니즘을 사용하도록 구성됩니다.",
"RestartReloadNote": "참고 : {appName}는 복원 프로세스 중에 UI를 자동으로 다시 시작하고 다시로드합니다.",
"UpdateAppDirectlyLoadError": "{appName}를 직접 업데이트 할 수 없습니다.",
"AptUpdater": "apt를 사용하여 업데이트 설치"
}

Some files were not shown because too many files have changed in this diff Show More