mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
48 Commits
v1.16.1.44
...
v1.17.2.45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5f2187c8 | ||
|
|
401ef88971 | ||
|
|
4fb3754048 | ||
|
|
596efe8fb0 | ||
|
|
076a4f2574 | ||
|
|
9561371a47 | ||
|
|
16254cf5f9 | ||
|
|
649a03e5a0 | ||
|
|
dd21d9b521 | ||
|
|
68b895d2ad | ||
|
|
634016ae1b | ||
|
|
83c6751847 | ||
|
|
04bb0c51b1 | ||
|
|
d2e9621de9 | ||
|
|
cb673ddc42 | ||
|
|
440618f2b6 | ||
|
|
ae79d45664 | ||
|
|
1877ccb513 | ||
|
|
b3098f2e4c | ||
|
|
3e0af062c1 | ||
|
|
858f85c50d | ||
|
|
938848be65 | ||
|
|
615f5899cc | ||
|
|
5a6b1313e8 | ||
|
|
ab7debb34b | ||
|
|
eee21de795 | ||
|
|
15fabbe7d0 | ||
|
|
6aef48c6e7 | ||
|
|
b29bc923fc | ||
|
|
b223e9b0cc | ||
|
|
77a982a7da | ||
|
|
ab3dc765b4 | ||
|
|
0261201360 | ||
|
|
1da3954879 | ||
|
|
742dd5ff54 | ||
|
|
a85406e3b7 | ||
|
|
73cdaf3d44 | ||
|
|
e26fa2dbf4 | ||
|
|
64be68a22d | ||
|
|
478a185968 | ||
|
|
4ff5d11a03 | ||
|
|
6000952b76 | ||
|
|
5fee2c4cd9 | ||
|
|
21d553cf1b | ||
|
|
782b2d3761 | ||
|
|
e1da3eee80 | ||
|
|
09af2da6b9 | ||
|
|
e3e9094d42 |
13
.devcontainer/Prowlarr.code-workspace
Normal file
13
.devcontainer/Prowlarr.code-workspace
Normal file
@@ -0,0 +1,13 @@
|
||||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
28
.github/labeler.yml
vendored
28
.github/labeler.yml
vendored
@@ -1,19 +1,31 @@
|
||||
'Area: API':
|
||||
- src/Prowlarr.Api.V1/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/Prowlarr.Api.V1/**/*
|
||||
|
||||
'Area: Db-migration':
|
||||
- src/NzbDrone.Core/Datastore/Migration/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Datastore/Migration/*
|
||||
|
||||
'Area: Download Clients':
|
||||
- src/NzbDrone.Core/Download/Clients/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Download/Clients/**/*
|
||||
|
||||
'Area: Indexer':
|
||||
- src/NzbDrone.Core/Indexers/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Indexers/**/*
|
||||
|
||||
'Area: Notifications':
|
||||
- src/NzbDrone.Core/Notifications/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/NzbDrone.Core/Notifications/**/*
|
||||
|
||||
'Area: UI':
|
||||
- frontend/**/*
|
||||
- package.json
|
||||
- yarn.lock
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- frontend/**/*
|
||||
- package.json
|
||||
- yarn.lock
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -9,4 +9,4 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
- uses: actions/labeler@v5
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.16.1'
|
||||
majorVersion: '1.17.2'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
dotnetVersion: '6.0.421'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
@@ -166,10 +166,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -1075,10 +1075,10 @@ stages:
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
||||
@@ -12,11 +12,10 @@ function App({ store, history }) {
|
||||
<DocumentTitle title={window.Prowlarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
||||
37
frontend/src/App/ApplyTheme.tsx
Normal file
37
frontend/src/App/ApplyTheme.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
interface ApplyThemeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
||||
@@ -42,7 +42,16 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
indexerHistory: IndexerHistoryAppState;
|
||||
|
||||
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
titleClassName,
|
||||
descriptionClassName,
|
||||
title,
|
||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<DescriptionListItemTitle
|
||||
className={titleClassName}
|
||||
>
|
||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
||||
}
|
||||
|
||||
DescriptionListItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
titleClassName: PropTypes.string,
|
||||
descriptionClassName: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
|
||||
@@ -256,6 +256,7 @@ FormInputGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
isFloat: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
min: PropTypes.number,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
font-family: $passwordFamily;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type="password"
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
className: styles.input
|
||||
...TextInput.props
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
||||
@@ -25,14 +25,3 @@
|
||||
font-family: 'Ubuntu Mono';
|
||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* text-security-disc
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: 'text-security-disc';
|
||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
enum DownloadProtocol {
|
||||
Unknown = 'unknown',
|
||||
Usenet = 'usenet',
|
||||
Torrent = 'torrent',
|
||||
}
|
||||
|
||||
export default DownloadProtocol;
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
faChevronCircleRight as fasChevronCircleRight,
|
||||
faChevronCircleUp as fasChevronCircleUp,
|
||||
faCircle as fasCircle,
|
||||
faCircleDown as fasCircleDown,
|
||||
faCloud as fasCloud,
|
||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||
faCog as fasCog,
|
||||
@@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
||||
export const CHECK_CIRCLE = fasCheckCircle;
|
||||
export const CHECK_SQUARE = fasSquareCheck;
|
||||
export const CIRCLE = fasCircle;
|
||||
export const CIRCLE_DOWN = fasCircleDown;
|
||||
export const CIRCLE_OUTLINE = farCircle;
|
||||
export const CLEAR = fasTrashAlt;
|
||||
export const CLIPBOARD = fasCopy;
|
||||
|
||||
@@ -21,6 +21,7 @@ function HistoryDetails(props) {
|
||||
limit,
|
||||
offset,
|
||||
source,
|
||||
host,
|
||||
url
|
||||
} = data;
|
||||
|
||||
@@ -86,6 +87,15 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
title={translate('Host')}
|
||||
data={host}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
data ?
|
||||
<DescriptionListItem
|
||||
|
||||
@@ -331,6 +331,21 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'host') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{
|
||||
data.host ?
|
||||
data.host :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'elapsedTime') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -224,6 +224,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
name="seedRatio"
|
||||
value={seedRatio}
|
||||
helpText={translate('SeedRatioHelpText')}
|
||||
isFloat={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -47,3 +47,42 @@ $hoverScale: 1.05;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.downloadLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin: 0 2px;
|
||||
width: 22px;
|
||||
color: var(--textColor);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.manualDownloadContent {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
width: 22px;
|
||||
height: 20.39px;
|
||||
vertical-align: middle;
|
||||
line-height: 20.39px;
|
||||
|
||||
&:hover {
|
||||
color: var(--iconButtonHoverColor);
|
||||
}
|
||||
}
|
||||
|
||||
.interactiveIcon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
/* width: 100%; */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.downloadIcon {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 8px;
|
||||
/* width: 100%; */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ interface CssExports {
|
||||
'actions': string;
|
||||
'container': string;
|
||||
'content': string;
|
||||
'downloadIcon': string;
|
||||
'downloadLink': string;
|
||||
'indexerRow': string;
|
||||
'info': string;
|
||||
'infoRow': string;
|
||||
'interactiveIcon': string;
|
||||
'manualDownloadContent': string;
|
||||
'title': string;
|
||||
'titleRow': string;
|
||||
}
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||
import Peers from 'Search/Table/Peers';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SearchIndexOverview.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
|
||||
function getContentHeight(rowHeight, isSmallScreen) {
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
|
||||
return rowHeight - padding;
|
||||
}
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed, grabError) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddedToDownloadClient');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddToDownloadClient');
|
||||
}
|
||||
|
||||
class SearchIndexOverview extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
infoUrl,
|
||||
protocol,
|
||||
downloadUrl,
|
||||
magnetUrl,
|
||||
categories,
|
||||
seeders,
|
||||
leechers,
|
||||
indexerFlags,
|
||||
size,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
indexer,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
isGrabbed,
|
||||
isGrabbing,
|
||||
grabError
|
||||
} = this.props;
|
||||
|
||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info} style={{ height: contentHeight }}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.title}>
|
||||
<Link
|
||||
to={infoUrl}
|
||||
title={title}
|
||||
>
|
||||
<TextTruncate
|
||||
line={2}
|
||||
text={title}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={this.onGrabPress}
|
||||
/>
|
||||
|
||||
{
|
||||
downloadUrl || magnetUrl ?
|
||||
<IconButton
|
||||
name={icons.SAVE}
|
||||
title={translate('Save')}
|
||||
to={downloadUrl ?? magnetUrl}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.indexerRow}>
|
||||
{indexer}
|
||||
</div>
|
||||
<div className={styles.infoRow}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
|
||||
{
|
||||
protocol === 'torrent' &&
|
||||
<Peers
|
||||
seeders={seeders}
|
||||
leechers={leechers}
|
||||
/>
|
||||
}
|
||||
|
||||
<Label>
|
||||
{formatBytes(size)}
|
||||
</Label>
|
||||
|
||||
<Label>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</Label>
|
||||
|
||||
<CategoryLabel
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
{
|
||||
indexerFlags.length ?
|
||||
indexerFlags
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((flag, index) => {
|
||||
return (
|
||||
<Label key={index} kind={kinds.INFO}>
|
||||
{titleCase(flag)}
|
||||
</Label>
|
||||
);
|
||||
}) :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchIndexOverview.propTypes = {
|
||||
guid: PropTypes.string.isRequired,
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
age: PropTypes.number.isRequired,
|
||||
ageHours: PropTypes.number.isRequired,
|
||||
ageMinutes: PropTypes.number.isRequired,
|
||||
publishDate: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
infoUrl: PropTypes.string.isRequired,
|
||||
downloadUrl: PropTypes.string,
|
||||
magnetUrl: PropTypes.string,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
indexer: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
files: PropTypes.number,
|
||||
grabs: PropTypes.number,
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number,
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isGrabbed: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.string
|
||||
};
|
||||
|
||||
SearchIndexOverview.defaultProps = {
|
||||
isGrabbing: false,
|
||||
isGrabbed: false
|
||||
};
|
||||
|
||||
export default SearchIndexOverview;
|
||||
262
frontend/src/Search/Mobile/SearchIndexOverview.tsx
Normal file
262
frontend/src/Search/Mobile/SearchIndexOverview.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
|
||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||
import Peers from 'Search/Table/Peers';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SearchIndexOverview.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(
|
||||
dimensions.movieIndexColumnPaddingSmallScreen
|
||||
);
|
||||
|
||||
function getDownloadIcon(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddedToDownloadClient');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddToDownloadClient');
|
||||
}
|
||||
|
||||
interface SearchIndexOverviewProps {
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
age: number;
|
||||
ageHours: number;
|
||||
ageMinutes: number;
|
||||
publishDate: string;
|
||||
title: string;
|
||||
infoUrl: string;
|
||||
downloadUrl?: string;
|
||||
magnetUrl?: string;
|
||||
indexerId: number;
|
||||
indexer: string;
|
||||
categories: IndexerCategory[];
|
||||
size: number;
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
indexerFlags: string[];
|
||||
isGrabbing: boolean;
|
||||
isGrabbed: boolean;
|
||||
grabError?: string;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
rowHeight: number;
|
||||
isSmallScreen: boolean;
|
||||
onGrabPress(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function SearchIndexOverview(props: SearchIndexOverviewProps) {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
protocol,
|
||||
categories,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishDate,
|
||||
title,
|
||||
infoUrl,
|
||||
downloadUrl,
|
||||
magnetUrl,
|
||||
indexer,
|
||||
size,
|
||||
seeders,
|
||||
leechers,
|
||||
indexerFlags = [],
|
||||
isGrabbing = false,
|
||||
isGrabbed = false,
|
||||
grabError,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
onGrabPress,
|
||||
} = props;
|
||||
|
||||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
|
||||
const { items: downloadClients } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
const onGrabPressWrapper = useCallback(() => {
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
});
|
||||
}, [guid, indexerId, onGrabPress]);
|
||||
|
||||
const onOverridePress = useCallback(() => {
|
||||
setIsOverrideModalOpen(true);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
const onOverrideModalClose = useCallback(() => {
|
||||
setIsOverrideModalOpen(false);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
const contentHeight = useMemo(() => {
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
|
||||
return rowHeight - padding;
|
||||
}, [rowHeight, isSmallScreen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info} style={{ height: contentHeight }}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.title}>
|
||||
<Link to={infoUrl} title={title}>
|
||||
<TextTruncate line={2} text={title} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPressWrapper}
|
||||
/>
|
||||
|
||||
{downloadClients.length > 1 ? (
|
||||
<Link
|
||||
className={styles.manualDownloadContent}
|
||||
title={translate('OverrideAndAddToDownloadClient')}
|
||||
onPress={onOverridePress}
|
||||
>
|
||||
<div className={styles.manualDownloadContent}>
|
||||
<Icon
|
||||
className={styles.interactiveIcon}
|
||||
name={icons.INTERACTIVE}
|
||||
size={11}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={styles.downloadIcon}
|
||||
name={icons.CIRCLE_DOWN}
|
||||
size={9}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
{downloadUrl || magnetUrl ? (
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.SAVE}
|
||||
title={translate('Save')}
|
||||
to={downloadUrl ?? magnetUrl}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.indexerRow}>{indexer}</div>
|
||||
<div className={styles.infoRow}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
|
||||
{protocol === 'torrent' && (
|
||||
<Peers seeders={seeders} leechers={leechers} />
|
||||
)}
|
||||
|
||||
<Label>{formatBytes(size)}</Label>
|
||||
|
||||
<Label
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||
includeSeconds: true,
|
||||
})}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</Label>
|
||||
|
||||
<CategoryLabel categories={categories} />
|
||||
|
||||
{indexerFlags.length
|
||||
? indexerFlags
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((flag, index) => {
|
||||
return (
|
||||
<Label key={index} kind={kinds.INFO}>
|
||||
{titleCase(flag)}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OverrideMatchModal
|
||||
isOpen={isOverrideModalOpen}
|
||||
title={title}
|
||||
indexerId={indexerId}
|
||||
guid={guid}
|
||||
protocol={protocol}
|
||||
isGrabbing={isGrabbing}
|
||||
grabError={grabError}
|
||||
onModalClose={onOverrideModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchIndexOverview;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import SelectDownloadClientModalContent from './SelectDownloadClientModalContent';
|
||||
|
||||
interface SelectDownloadClientModalProps {
|
||||
isOpen: boolean;
|
||||
protocol: DownloadProtocol;
|
||||
modalTitle: string;
|
||||
onDownloadClientSelect(downloadClientId: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
|
||||
const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||
<SelectDownloadClientModalContent
|
||||
protocol={protocol}
|
||||
modalTitle={modalTitle}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientModal;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectDownloadClientRow from './SelectDownloadClientRow';
|
||||
|
||||
interface SelectDownloadClientModalContentProps {
|
||||
protocol: DownloadProtocol;
|
||||
modalTitle: string;
|
||||
onDownloadClientSelect(downloadClientId: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectDownloadClientModalContent(
|
||||
props: SelectDownloadClientModalContentProps
|
||||
) {
|
||||
const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props;
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('SelectDownloadClientModalTitle', { modalTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('DownloadClientsLoadError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
<Form>
|
||||
{items.map((downloadClient) => {
|
||||
const { id, name, priority } = downloadClient;
|
||||
|
||||
return (
|
||||
<SelectDownloadClientRow
|
||||
key={id}
|
||||
id={id}
|
||||
name={name}
|
||||
priority={priority}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientModalContent;
|
||||
@@ -0,0 +1,6 @@
|
||||
.downloadClient {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'input': string;
|
||||
'downloadClient': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectDownloadClientRow.css';
|
||||
|
||||
interface SelectSeasonRowProps {
|
||||
id: number;
|
||||
name: string;
|
||||
priority: number;
|
||||
onDownloadClientSelect(downloadClientId: number): unknown;
|
||||
}
|
||||
|
||||
function SelectDownloadClientRow(props: SelectSeasonRowProps) {
|
||||
const { id, name, priority, onDownloadClientSelect } = props;
|
||||
|
||||
const onSeasonSelectWrapper = useCallback(() => {
|
||||
onDownloadClientSelect(id);
|
||||
}, [id, onDownloadClientSelect]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.downloadClient}
|
||||
component="div"
|
||||
onPress={onSeasonSelectWrapper}
|
||||
>
|
||||
<div>{name}</div>
|
||||
<div>{translate('PrioritySettings', { priority })}</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientRow;
|
||||
17
frontend/src/Search/OverrideMatch/OverrideMatchData.css
Normal file
17
frontend/src/Search/OverrideMatch/OverrideMatchData.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: inline-block;
|
||||
margin: -2px 0;
|
||||
width: 100%;
|
||||
outline: 2px dashed var(--dangerColor);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.optional {
|
||||
outline: 2px dashed var(--gray);
|
||||
}
|
||||
9
frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts
vendored
Normal file
9
frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'link': string;
|
||||
'optional': string;
|
||||
'placeholder': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
35
frontend/src/Search/OverrideMatch/OverrideMatchData.tsx
Normal file
35
frontend/src/Search/OverrideMatch/OverrideMatchData.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './OverrideMatchData.css';
|
||||
|
||||
interface OverrideMatchDataProps {
|
||||
value?: string | number | JSX.Element | JSX.Element[];
|
||||
isDisabled?: boolean;
|
||||
isOptional?: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function OverrideMatchData(props: OverrideMatchDataProps) {
|
||||
const { value, isDisabled = false, isOptional, onPress } = props;
|
||||
|
||||
return (
|
||||
<Link className={styles.link} isDisabled={isDisabled} onPress={onPress}>
|
||||
{(value == null || (Array.isArray(value) && value.length === 0)) &&
|
||||
!isDisabled ? (
|
||||
<span
|
||||
className={classNames(
|
||||
styles.placeholder,
|
||||
isOptional && styles.optional
|
||||
)}
|
||||
>
|
||||
|
||||
</span>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchData;
|
||||
45
frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx
Normal file
45
frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import OverrideMatchModalContent from './OverrideMatchModalContent';
|
||||
|
||||
interface OverrideMatchModalProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
indexerId: number;
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function OverrideMatchModal(props: OverrideMatchModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
title,
|
||||
indexerId,
|
||||
guid,
|
||||
protocol,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||
<OverrideMatchModalContent
|
||||
title={title}
|
||||
indexerId={indexerId}
|
||||
guid={guid}
|
||||
protocol={protocol}
|
||||
isGrabbing={isGrabbing}
|
||||
grabError={grabError}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchModal;
|
||||
@@ -0,0 +1,49 @@
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-right: 20px;
|
||||
color: var(--dangerColor);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
11
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts
vendored
Normal file
11
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'buttons': string;
|
||||
'error': string;
|
||||
'footer': string;
|
||||
'item': string;
|
||||
'label': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
150
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx
Normal file
150
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
|
||||
import OverrideMatchData from './OverrideMatchData';
|
||||
import styles from './OverrideMatchModalContent.css';
|
||||
|
||||
type SelectType = 'select' | 'downloadClient';
|
||||
|
||||
interface OverrideMatchModalContentProps {
|
||||
indexerId: number;
|
||||
title: string;
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
||||
const modalTitle = translate('ManualGrab');
|
||||
const {
|
||||
indexerId,
|
||||
title,
|
||||
guid,
|
||||
protocol,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
|
||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||
null
|
||||
);
|
||||
const previousIsGrabbing = usePrevious(isGrabbing);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { items: downloadClients } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
const onSelectModalClose = useCallback(() => {
|
||||
setSelectModalOpen(null);
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSelectDownloadClientPress = useCallback(() => {
|
||||
setSelectModalOpen('downloadClient');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onDownloadClientSelect = useCallback(
|
||||
(downloadClientId: number) => {
|
||||
setDownloadClientId(downloadClientId);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setDownloadClientId, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onGrabPress = useCallback(() => {
|
||||
dispatch(
|
||||
grabRelease({
|
||||
indexerId,
|
||||
guid,
|
||||
downloadClientId,
|
||||
})
|
||||
);
|
||||
}, [indexerId, guid, downloadClientId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGrabbing && previousIsGrabbing) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isGrabbing, previousIsGrabbing, onModalClose]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
dispatch(fetchDownloadClients());
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('OverrideGrabModalTitle', { title })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
{downloadClients.length > 1 ? (
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('DownloadClient')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={
|
||||
downloadClients.find(
|
||||
(downloadClient) => downloadClient.id === downloadClientId
|
||||
)?.name ?? translate('Default')
|
||||
}
|
||||
onPress={onSelectDownloadClientPress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.footer}>
|
||||
<div className={styles.error}>{grabError}</div>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isGrabbing}
|
||||
error={grabError}
|
||||
onPress={onGrabPress}
|
||||
>
|
||||
{translate('GrabRelease')}
|
||||
</SpinnerErrorButton>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<SelectDownloadClientModal
|
||||
isOpen={selectModalOpen === 'downloadClient'}
|
||||
protocol={protocol}
|
||||
modalTitle={modalTitle}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchModalContent;
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
|
||||
import { fetchDownloadClients } from 'Store/Actions/Settings/downloadClients';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
|
||||
import SearchIndex from './SearchIndex';
|
||||
@@ -55,12 +56,20 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
|
||||
dispatchClearReleases() {
|
||||
dispatch(clearReleases());
|
||||
},
|
||||
|
||||
dispatchFetchDownloadClients() {
|
||||
dispatch(fetchDownloadClients());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class SearchIndexConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchDownloadClients();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchCancelFetchReleases();
|
||||
this.props.dispatchClearReleases();
|
||||
@@ -85,6 +94,7 @@ SearchIndexConnector.propTypes = {
|
||||
onBulkGrabPress: PropTypes.func.isRequired,
|
||||
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchClearReleases: PropTypes.func.isRequired,
|
||||
dispatchFetchDownloadClients: PropTypes.func.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
|
||||
function createReleaseSelector() {
|
||||
return createSelector(
|
||||
@@ -37,10 +36,6 @@ function createMapStateToProps() {
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class SearchIndexItemConnector extends Component {
|
||||
|
||||
//
|
||||
@@ -71,4 +66,4 @@ SearchIndexItemConnector.propTypes = {
|
||||
component: PropTypes.elementType.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
||||
export default connect(createMapStateToProps, null)(SearchIndexItemConnector);
|
||||
|
||||
@@ -67,3 +67,33 @@
|
||||
|
||||
color: var(--textColor);
|
||||
}
|
||||
|
||||
.manualDownloadContent {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
width: 22px;
|
||||
height: 20.39px;
|
||||
vertical-align: middle;
|
||||
line-height: 20.39px;
|
||||
|
||||
&:hover {
|
||||
color: var(--iconButtonHoverColor);
|
||||
}
|
||||
}
|
||||
|
||||
.interactiveIcon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
/* width: 100%; */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.downloadIcon {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 8px;
|
||||
/* width: 100%; */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ interface CssExports {
|
||||
'category': string;
|
||||
'cell': string;
|
||||
'checkInput': string;
|
||||
'downloadIcon': string;
|
||||
'downloadLink': string;
|
||||
'externalLinks': string;
|
||||
'files': string;
|
||||
'grabs': string;
|
||||
'indexer': string;
|
||||
'indexerFlags': string;
|
||||
'interactiveIcon': string;
|
||||
'manualDownloadContent': string;
|
||||
'peers': string;
|
||||
'protocol': string;
|
||||
'size': string;
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CategoryLabel from './CategoryLabel';
|
||||
import Peers from './Peers';
|
||||
import ReleaseLinks from './ReleaseLinks';
|
||||
import styles from './SearchIndexRow.css';
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed, grabError) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddedToDownloadClient');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddToDownloadClient');
|
||||
}
|
||||
|
||||
class SearchIndexRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmGrabModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId
|
||||
});
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
downloadUrl,
|
||||
fileName,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
onSavePress({
|
||||
downloadUrl,
|
||||
fileName
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
guid,
|
||||
protocol,
|
||||
downloadUrl,
|
||||
magnetUrl,
|
||||
categories,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishDate,
|
||||
title,
|
||||
infoUrl,
|
||||
indexer,
|
||||
size,
|
||||
files,
|
||||
grabs,
|
||||
seeders,
|
||||
leechers,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
indexerFlags,
|
||||
columns,
|
||||
isGrabbing,
|
||||
isGrabbed,
|
||||
grabError,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (column.name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.checkInput}
|
||||
id={guid}
|
||||
key={column.name}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'protocol') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'age') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'sortTitle') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
<Link
|
||||
to={infoUrl}
|
||||
title={title}
|
||||
>
|
||||
<div>
|
||||
{title}
|
||||
</div>
|
||||
</Link>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'indexer') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{indexer}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'size') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{formatBytes(size)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'files') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{files}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'grabs') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{grabs}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'peers') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{
|
||||
protocol === 'torrent' &&
|
||||
<Peers
|
||||
seeders={seeders}
|
||||
leechers={leechers}
|
||||
/>
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'category') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
<CategoryLabel
|
||||
categories={categories}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'indexerFlags') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{
|
||||
!!indexerFlags.length &&
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.FLAG}
|
||||
kind={kinds.PRIMARY}
|
||||
/>
|
||||
}
|
||||
title={translate('IndexerFlags')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
indexerFlags.map((flag, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{titleCase(flag)}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={column.name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={this.onGrabPress}
|
||||
/>
|
||||
|
||||
{
|
||||
downloadUrl ?
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.SAVE}
|
||||
title={translate('Save')}
|
||||
onPress={this.onSavePress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
magnetUrl ?
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.MAGNET}
|
||||
title={translate('Open')}
|
||||
to={magnetUrl}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
imdbId || tmdbId || tvdbId || tvMazeId ? (
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.externalLinks}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={12}
|
||||
/>
|
||||
}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<ReleaseLinks
|
||||
categories={categories}
|
||||
imdbId={imdbId}
|
||||
tmdbId={tmdbId}
|
||||
tvdbId={tvdbId}
|
||||
tvMazeId={tvMazeId}
|
||||
/>
|
||||
}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchIndexRow.propTypes = {
|
||||
guid: PropTypes.string.isRequired,
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
age: PropTypes.number.isRequired,
|
||||
ageHours: PropTypes.number.isRequired,
|
||||
ageMinutes: PropTypes.number.isRequired,
|
||||
publishDate: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
fileName: PropTypes.string.isRequired,
|
||||
infoUrl: PropTypes.string.isRequired,
|
||||
downloadUrl: PropTypes.string,
|
||||
magnetUrl: PropTypes.string,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
indexer: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
files: PropTypes.number,
|
||||
grabs: PropTypes.number,
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number,
|
||||
imdbId: PropTypes.number,
|
||||
tmdbId: PropTypes.number,
|
||||
tvdbId: PropTypes.number,
|
||||
tvMazeId: PropTypes.number,
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isGrabbed: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.string,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SearchIndexRow.defaultProps = {
|
||||
isGrabbing: false,
|
||||
isGrabbed: false
|
||||
};
|
||||
|
||||
export default SearchIndexRow;
|
||||
395
frontend/src/Search/Table/SearchIndexRow.tsx
Normal file
395
frontend/src/Search/Table/SearchIndexRow.tsx
Normal file
@@ -0,0 +1,395 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CategoryLabel from './CategoryLabel';
|
||||
import Peers from './Peers';
|
||||
import ReleaseLinks from './ReleaseLinks';
|
||||
import styles from './SearchIndexRow.css';
|
||||
|
||||
function getDownloadIcon(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddedToDownloadClient');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddToDownloadClient');
|
||||
}
|
||||
|
||||
interface SearchIndexRowProps {
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
age: number;
|
||||
ageHours: number;
|
||||
ageMinutes: number;
|
||||
publishDate: string;
|
||||
title: string;
|
||||
fileName: string;
|
||||
infoUrl: string;
|
||||
downloadUrl?: string;
|
||||
magnetUrl?: string;
|
||||
indexerId: number;
|
||||
indexer: string;
|
||||
categories: IndexerCategory[];
|
||||
size: number;
|
||||
files?: number;
|
||||
grabs?: number;
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
imdbId?: string;
|
||||
tmdbId?: number;
|
||||
tvdbId?: number;
|
||||
tvMazeId?: number;
|
||||
indexerFlags: string[];
|
||||
isGrabbing: boolean;
|
||||
isGrabbed: boolean;
|
||||
grabError?: string;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
columns: Column[];
|
||||
isSelected?: boolean;
|
||||
onSelectedChange(result: SelectStateInputProps): void;
|
||||
onGrabPress(...args: unknown[]): void;
|
||||
onSavePress(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function SearchIndexRow(props: SearchIndexRowProps) {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
protocol,
|
||||
categories,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishDate,
|
||||
title,
|
||||
fileName,
|
||||
infoUrl,
|
||||
downloadUrl,
|
||||
magnetUrl,
|
||||
indexer,
|
||||
size,
|
||||
files,
|
||||
grabs,
|
||||
seeders,
|
||||
leechers,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
indexerFlags = [],
|
||||
isGrabbing = false,
|
||||
isGrabbed = false,
|
||||
grabError,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
columns,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
onGrabPress,
|
||||
onSavePress,
|
||||
} = props;
|
||||
|
||||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
|
||||
const { items: downloadClients } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
const onGrabPressWrapper = useCallback(() => {
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
});
|
||||
}, [guid, indexerId, onGrabPress]);
|
||||
|
||||
const onSavePressWrapper = useCallback(() => {
|
||||
onSavePress({
|
||||
downloadUrl,
|
||||
fileName,
|
||||
});
|
||||
}, [downloadUrl, fileName, onSavePress]);
|
||||
|
||||
const onOverridePress = useCallback(() => {
|
||||
setIsOverrideModalOpen(true);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
const onOverrideModalClose = useCallback(() => {
|
||||
setIsOverrideModalOpen(false);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.checkInput}
|
||||
id={guid}
|
||||
key={name}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'age') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||
includeSeconds: true,
|
||||
})}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sortTitle') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<Link to={infoUrl} title={title}>
|
||||
<div>{title}</div>
|
||||
</Link>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{indexer}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{formatBytes(size)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'files') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{files}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'grabs') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{grabs}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'peers') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{protocol === 'torrent' && (
|
||||
<Peers seeders={seeders} leechers={leechers} />
|
||||
)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'category') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<CategoryLabel categories={categories} />
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexerFlags') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{!!indexerFlags.length && (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={
|
||||
<ul>
|
||||
{indexerFlags.map((flag, index) => {
|
||||
return <li key={index}>{titleCase(flag)}</li>;
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPressWrapper}
|
||||
/>
|
||||
|
||||
{downloadClients.length > 1 ? (
|
||||
<Link
|
||||
className={styles.manualDownloadContent}
|
||||
title={translate('OverrideAndAddToDownloadClient')}
|
||||
onPress={onOverridePress}
|
||||
>
|
||||
<div className={styles.manualDownloadContent}>
|
||||
<Icon
|
||||
className={styles.interactiveIcon}
|
||||
name={icons.INTERACTIVE}
|
||||
size={12}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={styles.downloadIcon}
|
||||
name={icons.CIRCLE_DOWN}
|
||||
size={10}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
{downloadUrl ? (
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.SAVE}
|
||||
title={translate('Save')}
|
||||
onPress={onSavePressWrapper}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{magnetUrl ? (
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.MAGNET}
|
||||
title={translate('Open')}
|
||||
to={magnetUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{imdbId || tmdbId || tvdbId || tvMazeId ? (
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.externalLinks}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={12}
|
||||
/>
|
||||
}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<ReleaseLinks
|
||||
categories={categories}
|
||||
imdbId={imdbId}
|
||||
tmdbId={tmdbId}
|
||||
tvdbId={tvdbId}
|
||||
tvMazeId={tvMazeId}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
) : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
<OverrideMatchModal
|
||||
isOpen={isOverrideModalOpen}
|
||||
title={title}
|
||||
indexerId={indexerId}
|
||||
guid={guid}
|
||||
protocol={protocol}
|
||||
isGrabbing={isGrabbing}
|
||||
grabError={grabError}
|
||||
onModalClose={onOverrideModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchIndexRow;
|
||||
@@ -1,8 +1,11 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastTestData = null;
|
||||
|
||||
export function createCancelTestProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
@@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
||||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isTesting: true }));
|
||||
|
||||
const testData = getProviderState(payload, getState, section);
|
||||
const {
|
||||
queryParams = {},
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const testData = getProviderState({ ...otherPayload }, getState, section);
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-testing the same provider without changes
|
||||
// force it to be tested.
|
||||
|
||||
if (_.isEqual(testData, lastTestData)) {
|
||||
params.forceTest = true;
|
||||
}
|
||||
|
||||
lastTestData = testData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/test`,
|
||||
url: `${url}/test?${$.param(params, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
@@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
||||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastTestData = null;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isTesting: false,
|
||||
|
||||
@@ -82,6 +82,12 @@ export const defaultState = {
|
||||
isSortable: false,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'host',
|
||||
label: () => translate('Host'),
|
||||
isSortable: false,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'elapsedTime',
|
||||
label: () => translate('ElapsedTime'),
|
||||
|
||||
@@ -401,7 +401,16 @@ export const actionHandlers = handleThunks({
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_RELEASES]: (state) => {
|
||||
return Object.assign({}, state, defaultState);
|
||||
const {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
customFilters,
|
||||
selectedFilterKey,
|
||||
columns,
|
||||
...otherDefaultState
|
||||
} = defaultState;
|
||||
|
||||
return Object.assign({}, state, otherDefaultState);
|
||||
},
|
||||
|
||||
[UPDATE_RELEASE]: (state, { payload }) => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createDimensionsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.app.dimensions,
|
||||
(state: AppState) => state.app.dimensions,
|
||||
(dimensions) => {
|
||||
return dimensions;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
|
||||
export default function createEnabledDownloadClientsSelector(
|
||||
protocol: DownloadProtocol
|
||||
) {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||
(downloadClients: DownloadClientAppState) => {
|
||||
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||
|
||||
const clients = items.filter(
|
||||
(item) => item.protocol === protocol && item.enable
|
||||
);
|
||||
|
||||
return { isFetching, isPopulated, error, items: clients };
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -188,7 +188,7 @@ module.exports = {
|
||||
// Charts
|
||||
|
||||
chartBackgroundColor: '#262626',
|
||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
||||
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
|
||||
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','),
|
||||
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','),
|
||||
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as dark from './dark';
|
||||
import * as light from './light';
|
||||
|
||||
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const auto = defaultDark ? { ...dark } : { ...light };
|
||||
const auto = defaultDark ? dark : light;
|
||||
|
||||
export default {
|
||||
auto,
|
||||
|
||||
@@ -188,7 +188,7 @@ module.exports = {
|
||||
// Charts
|
||||
|
||||
chartBackgroundColor: '#fff',
|
||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
||||
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
|
||||
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','),
|
||||
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','),
|
||||
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ module.exports = {
|
||||
// Families
|
||||
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
|
||||
passwordFamily: 'text-security-disc',
|
||||
|
||||
// Sizes
|
||||
extraSmallFontSize: '11px',
|
||||
|
||||
@@ -7,10 +7,10 @@ function formatBytes(input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return filesize(size, {
|
||||
return `${filesize(size, {
|
||||
base: 2,
|
||||
round: 1
|
||||
});
|
||||
})}`;
|
||||
}
|
||||
|
||||
export default formatBytes;
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f5f7fa;
|
||||
color: #656565;
|
||||
background-color: var(--pageBackground);
|
||||
color: var(--textColor);
|
||||
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
@@ -88,14 +88,14 @@
|
||||
padding: 10px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
background-color: #464b51;
|
||||
background-color: var(--themeDarkColor);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 20px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fff;
|
||||
background-color: var(--panelBackground);
|
||||
}
|
||||
|
||||
.sign-in {
|
||||
@@ -112,16 +112,18 @@
|
||||
padding: 6px 16px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 1px solid #dde6e9;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
|
||||
color: var(--textColor);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: 0;
|
||||
border-color: #66afe9;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px rgba(102, 175, 233, 0.6);
|
||||
border-color: var(--inputFocusBorderColor);
|
||||
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
|
||||
0 0 8px var(--inputFocusBoxShadowColor);
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -130,10 +132,10 @@
|
||||
padding: 10px 0;
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
border-color: #5899eb;
|
||||
border-color: var(--primaryBorderColor);
|
||||
border-radius: 4px;
|
||||
background-color: #5d9cec;
|
||||
color: #fff;
|
||||
background-color: var(--primaryBackgroundColor);
|
||||
color: var(--white);
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
@@ -141,9 +143,9 @@
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
border-color: #3483e7;
|
||||
background-color: #4b91ea;
|
||||
color: #fff;
|
||||
border-color: var(--primaryHoverBorderColor);
|
||||
background-color: var(--primaryHoverBackgroundColor);
|
||||
color: var(--white);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -165,24 +167,24 @@
|
||||
|
||||
.forgot-password {
|
||||
margin-left: auto;
|
||||
color: #909fa7;
|
||||
color: var(--forgotPasswordColor);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.forgot-password:focus,
|
||||
.forgot-password:hover {
|
||||
color: #748690;
|
||||
color: var(--forgotPasswordAltColor);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.forgot-password:visited {
|
||||
color: #748690;
|
||||
color: var(--forgotPasswordAltColor);
|
||||
}
|
||||
|
||||
.login-failed {
|
||||
margin-top: 20px;
|
||||
color: #f05050;
|
||||
color: var(--failedColor);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -291,5 +293,59 @@
|
||||
|
||||
loginFailedDiv.classList.remove("hidden");
|
||||
}
|
||||
|
||||
var light = {
|
||||
white: '#fff',
|
||||
pageBackground: '#f5f7fa',
|
||||
textColor: '#515253',
|
||||
themeDarkColor: '#464b51',
|
||||
panelBackground: '#fff',
|
||||
inputBackgroundColor: '#fff',
|
||||
inputBorderColor: '#dde6e9',
|
||||
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||
inputFocusBorderColor: '#66afe9',
|
||||
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||
primaryBackgroundColor: '#5d9cec',
|
||||
primaryBorderColor: '#5899eb',
|
||||
primaryHoverBackgroundColor: '#4b91ea',
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
failedColor: '#f05050',
|
||||
forgotPasswordColor: '#909fa7',
|
||||
forgotPasswordAltColor: '#748690'
|
||||
};
|
||||
|
||||
var dark = {
|
||||
white: '#fff',
|
||||
pageBackground: '#202020',
|
||||
textColor: '#ccc',
|
||||
themeDarkColor: '#494949',
|
||||
panelBackground: '#111',
|
||||
inputBackgroundColor: '#333',
|
||||
inputBorderColor: '#dde6e9',
|
||||
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||
inputFocusBorderColor: '#66afe9',
|
||||
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||
primaryBackgroundColor: '#5d9cec',
|
||||
primaryBorderColor: '#5899eb',
|
||||
primaryHoverBackgroundColor: '#4b91ea',
|
||||
primaryHoverBorderColor: '#3483e7',
|
||||
failedColor: '#f05050',
|
||||
forgotPasswordColor: '#737d83',
|
||||
forgotPasswordAltColor: '#546067'
|
||||
};
|
||||
|
||||
var theme = "_THEME_";
|
||||
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
|
||||
dark :
|
||||
light;
|
||||
|
||||
Object.entries(finalTheme).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--${key}`,
|
||||
value
|
||||
);
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface UiSettings {
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
|
||||
44
package.json
44
package.json
@@ -30,12 +30,12 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"chart.js": "4.3.0",
|
||||
"@sentry/browser": "7.100.0",
|
||||
"@sentry/integrations": "7.100.0",
|
||||
"@types/node": "18.19.31",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"chart.js": "4.4.2",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
@@ -83,46 +83,46 @@
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.7",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "5.0.4"
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.11",
|
||||
"@babel/eslint-parser": "7.22.11",
|
||||
"@babel/plugin-proposal-export-default-from": "7.22.5",
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.22.14",
|
||||
"@babel/preset-react": "7.22.5",
|
||||
"@babel/preset-typescript": "7.22.11",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@types/lodash": "4.14.194",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"are-you-es5": "2.1.2",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.33.0",
|
||||
"core-js": "3.37.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"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",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.31",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
|
||||
@@ -99,20 +99,52 @@
|
||||
<RootNamespace Condition="'$(ProwlarrProject)'=='true'">$(MSBuildProjectName.Replace('Prowlarr','NzbDrone'))</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
||||
<!-- FXCop Built into Net5 SDK now as NETAnalyzers, Enabled by default on net5 projects -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<ItemGroup Condition="'$(TestProject)'!='true'">
|
||||
<!-- Annotates .NET assemblies with repository information including SHA -->
|
||||
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
||||
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Sentry specific configuration: Only in Release mode -->
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
||||
<!-- OrgSlug, ProjectSlug and AuthToken are required.
|
||||
They can be set below, via argument to 'msbuild -p:' or environment variable -->
|
||||
<SentryOrg></SentryOrg>
|
||||
<SentryProject></SentryProject>
|
||||
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
|
||||
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
|
||||
|
||||
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
|
||||
without the need to deploy the application with PDBs -->
|
||||
<SentryUploadSymbols>true</SentryUploadSymbols>
|
||||
|
||||
<!-- Source Link settings -->
|
||||
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
|
||||
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
|
||||
<EmbedAllSources>true</EmbedAllSources>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
||||
<!-- FXCop Built into Net5 SDK now as NETAnalyzers, Enabled by default on net5 projects -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Set up stylecop -->
|
||||
<ItemGroup Condition="'$(ProwlarrProject)'=='true' and '$(EnableAnalyzers)'!='false'">
|
||||
<!-- StyleCop analysis -->
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Test.Common;
|
||||
@@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
|
||||
.Callback<string, string>((p, t) => _configFileContents = t);
|
||||
|
||||
Mocker.GetMock<IOptions<AuthOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new AuthOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<AppOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new AppOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<ServerOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new ServerOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<LogOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new LogOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<UpdateOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new UpdateOptions());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NLog;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Sentry;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
@@ -26,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
|
||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
|
||||
}
|
||||
|
||||
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Test.Common;
|
||||
@@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test
|
||||
[TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")]
|
||||
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
|
||||
public void Clean_Path_Windows(string dirty, string clean)
|
||||
{
|
||||
@@ -315,5 +316,30 @@ namespace NzbDrone.Common.Test
|
||||
result[2].Should().Be(@"TV");
|
||||
result[3].Should().Be(@"Series Title");
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test\")]
|
||||
[TestCase(@"C:\Test")]
|
||||
[TestCase(@"C:\Test\TV\")]
|
||||
[TestCase(@"C:\Test\TV")]
|
||||
public void IsPathValid_should_be_true(string path)
|
||||
{
|
||||
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test \")]
|
||||
[TestCase(@"C:\Test ")]
|
||||
[TestCase(@"C:\ Test\")]
|
||||
[TestCase(@"C:\ Test")]
|
||||
[TestCase(@"C:\Test \TV")]
|
||||
[TestCase(@"C:\ Test\TV")]
|
||||
[TestCase(@"C:\Test \TV\")]
|
||||
[TestCase(@"C:\ Test\TV\")]
|
||||
[TestCase(@" C:\Test\TV\")]
|
||||
[TestCase(@" C:\Test\TV")]
|
||||
|
||||
public void IsPathValid_should_be_false(string path)
|
||||
{
|
||||
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using NUnit.Framework;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
@@ -29,10 +30,16 @@ namespace NzbDrone.Common.Test
|
||||
.AddNzbDroneLogger()
|
||||
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
||||
.AddDummyDatabase()
|
||||
.AddDummyLogDatabase()
|
||||
.AddStartupContext(new StartupContext("first", "second"));
|
||||
|
||||
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object);
|
||||
|
||||
var serviceProvider = container.GetServiceProvider();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
@@ -28,6 +29,12 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
public static string CleanFilePath(this string path)
|
||||
{
|
||||
if (path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
// Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix.
|
||||
path = path.TrimEnd(' ');
|
||||
}
|
||||
|
||||
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
|
||||
|
||||
@@ -36,10 +43,10 @@ namespace NzbDrone.Common.Extensions
|
||||
// UNC
|
||||
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
|
||||
{
|
||||
return info.FullName.TrimEnd('/', '\\', ' ');
|
||||
return info.FullName.TrimEnd('/', '\\');
|
||||
}
|
||||
|
||||
return info.FullName.TrimEnd('/').Trim('\\', ' ');
|
||||
return info.FullName.TrimEnd('/').Trim('\\');
|
||||
}
|
||||
|
||||
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
|
||||
@@ -143,6 +150,23 @@ namespace NzbDrone.Common.Extensions
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path.Trim() != path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
|
||||
while (directoryInfo != null)
|
||||
{
|
||||
if (directoryInfo.Name.Trim() != directoryInfo.Name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
directoryInfo = directoryInfo.Parent;
|
||||
}
|
||||
|
||||
if (validationType == PathValidationType.AnyOs)
|
||||
{
|
||||
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
|
||||
@@ -253,6 +277,11 @@ namespace NzbDrone.Common.Extensions
|
||||
return processName;
|
||||
}
|
||||
|
||||
public static string CleanPath(this string path)
|
||||
{
|
||||
return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray());
|
||||
}
|
||||
|
||||
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return appFolderInfo.AppDataFolder;
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
RegisterDebugger();
|
||||
}
|
||||
|
||||
RegisterSentry(updateApp);
|
||||
RegisterSentry(updateApp, appFolderInfo);
|
||||
|
||||
if (updateApp)
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
|
||||
private static void RegisterSentry(bool updateClient)
|
||||
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
string dsn;
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
: "https://e38306161ff945999adf774a16e933c3@sentry.servarr.com/30";
|
||||
}
|
||||
|
||||
var target = new SentryTarget(dsn)
|
||||
var target = new SentryTarget(dsn, appFolderInfo)
|
||||
{
|
||||
Name = "sentryTarget",
|
||||
Layout = "${message}"
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
public bool FilterEvents { get; set; }
|
||||
public bool SentryEnabled { get; set; }
|
||||
|
||||
public SentryTarget(string dsn)
|
||||
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
_sdk = SentrySdk.Init(o =>
|
||||
{
|
||||
@@ -114,9 +114,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.AttachStacktrace = true;
|
||||
o.MaxBreadcrumbs = 200;
|
||||
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
|
||||
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
|
||||
o.Environment = BuildInfo.Branch;
|
||||
|
||||
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
||||
o.AutoSessionTracking = true;
|
||||
|
||||
// Caches files in the event device is offline
|
||||
// Sentry creates a 'sentry' sub directory, no need to concat here
|
||||
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
|
||||
|
||||
// default environment is production
|
||||
if (!RuntimeInfo.IsProduction)
|
||||
{
|
||||
if (RuntimeInfo.IsDevelopment)
|
||||
{
|
||||
o.Environment = "development";
|
||||
}
|
||||
else if (RuntimeInfo.IsTesting)
|
||||
{
|
||||
o.Environment = "testing";
|
||||
}
|
||||
else
|
||||
{
|
||||
o.Environment = "other";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
InitializeScope();
|
||||
@@ -134,7 +158,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.User = new User
|
||||
scope.User = new SentryUser
|
||||
{
|
||||
Id = HashUtil.AnonymousToken()
|
||||
};
|
||||
@@ -317,13 +341,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
}
|
||||
}
|
||||
|
||||
var level = LoggingLevelMap[logEvent.Level];
|
||||
var sentryEvent = new SentryEvent(logEvent.Exception)
|
||||
{
|
||||
Level = LoggingLevelMap[logEvent.Level],
|
||||
Level = level,
|
||||
Logger = logEvent.LoggerName,
|
||||
Message = logEvent.FormattedMessage
|
||||
};
|
||||
|
||||
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
|
||||
{
|
||||
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
|
||||
// the 'unhandled' exception flag
|
||||
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
|
||||
}
|
||||
|
||||
sentryEvent.SetExtras(extras);
|
||||
sentryEvent.SetFingerprint(fingerPrint);
|
||||
|
||||
|
||||
8
src/NzbDrone.Common/Options/AppOptions.cs
Normal file
8
src/NzbDrone.Common/Options/AppOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class AppOptions
|
||||
{
|
||||
public string InstanceName { get; set; }
|
||||
public string Theme { get; set; }
|
||||
public bool? LaunchBrowser { get; set; }
|
||||
}
|
||||
9
src/NzbDrone.Common/Options/AuthOptions.cs
Normal file
9
src/NzbDrone.Common/Options/AuthOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class AuthOptions
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public bool? Enabled { get; set; }
|
||||
public string Method { get; set; }
|
||||
public string Required { get; set; }
|
||||
}
|
||||
15
src/NzbDrone.Common/Options/LogOptions.cs
Normal file
15
src/NzbDrone.Common/Options/LogOptions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class LogOptions
|
||||
{
|
||||
public string Level { get; set; }
|
||||
public bool? FilterSentryEvents { get; set; }
|
||||
public int? Rotate { get; set; }
|
||||
public bool? Sql { get; set; }
|
||||
public string ConsoleLevel { get; set; }
|
||||
public bool? AnalyticsEnabled { get; set; }
|
||||
public string SyslogServer { get; set; }
|
||||
public int? SyslogPort { get; set; }
|
||||
public string SyslogLevel { get; set; }
|
||||
public bool? DbEnabled { get; set; }
|
||||
}
|
||||
12
src/NzbDrone.Common/Options/ServerOptions.cs
Normal file
12
src/NzbDrone.Common/Options/ServerOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class ServerOptions
|
||||
{
|
||||
public string UrlBase { get; set; }
|
||||
public string BindAddress { get; set; }
|
||||
public int? Port { get; set; }
|
||||
public bool? EnableSsl { get; set; }
|
||||
public int? SslPort { get; set; }
|
||||
public string SslCertPath { get; set; }
|
||||
public string SslCertPassword { get; set; }
|
||||
}
|
||||
9
src/NzbDrone.Common/Options/UpdateOptions.cs
Normal file
9
src/NzbDrone.Common/Options/UpdateOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class UpdateOptions
|
||||
{
|
||||
public string Mechanism { get; set; }
|
||||
public bool? Automatically { get; set; }
|
||||
public string ScriptPath { get; set; }
|
||||
public string Branch { get; set; }
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<PackageReference Include="NLog" Version="5.2.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.6" />
|
||||
<PackageReference Include="Sentry" Version="3.29.1" />
|
||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().BeNull();
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("S01E03");
|
||||
query.Name.Should().Be("S01E03%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
|
||||
query.Tvrage.Should().BeNull();
|
||||
query.Search.Should().Be("Malcolm%in%the%Middle");
|
||||
query.Category.Should().Be("Episode");
|
||||
query.Name.Should().Be("S02E03");
|
||||
query.Name.Should().Be("S02E03%");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Dapper" Version="2.0.151" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="YamlDotNet" Version="13.1.1" />
|
||||
|
||||
@@ -10,9 +10,9 @@ namespace NzbDrone.Core.Applications
|
||||
void DeleteAllForApp(int appId);
|
||||
}
|
||||
|
||||
public class TagRepository : BasicRepository<AppIndexerMap>, IAppIndexerMapRepository
|
||||
public class AppIndexerMapRepository : BasicRepository<AppIndexerMap>, IAppIndexerMapRepository
|
||||
{
|
||||
public TagRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
public AppIndexerMapRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Datastore;
|
||||
@@ -53,13 +54,14 @@ namespace NzbDrone.Core.Configuration
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string SyslogLevel { get; }
|
||||
bool LogDbEnabled { get; }
|
||||
string Theme { get; }
|
||||
string PostgresHost { get; }
|
||||
int PostgresPort { get; }
|
||||
string PostgresUser { get; }
|
||||
string PostgresPassword { get; }
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
string Theme { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -72,6 +74,11 @@ namespace NzbDrone.Core.Configuration
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly ICached<string> _cache;
|
||||
private readonly PostgresOptions _postgresOptions;
|
||||
private readonly AuthOptions _authOptions;
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly ServerOptions _serverOptions;
|
||||
private readonly UpdateOptions _updateOptions;
|
||||
private readonly LogOptions _logOptions;
|
||||
|
||||
private readonly string _configFile;
|
||||
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
@@ -82,13 +89,23 @@ namespace NzbDrone.Core.Configuration
|
||||
ICacheManager cacheManager,
|
||||
IEventAggregator eventAggregator,
|
||||
IDiskProvider diskProvider,
|
||||
IOptions<PostgresOptions> postgresOptions)
|
||||
IOptions<PostgresOptions> postgresOptions,
|
||||
IOptions<AuthOptions> authOptions,
|
||||
IOptions<AppOptions> appOptions,
|
||||
IOptions<ServerOptions> serverOptions,
|
||||
IOptions<UpdateOptions> updateOptions,
|
||||
IOptions<LogOptions> logOptions)
|
||||
{
|
||||
_cache = cacheManager.GetCache<string>(GetType());
|
||||
_eventAggregator = eventAggregator;
|
||||
_diskProvider = diskProvider;
|
||||
_configFile = appFolderInfo.GetConfigPath();
|
||||
_postgresOptions = postgresOptions.Value;
|
||||
_authOptions = authOptions.Value;
|
||||
_appOptions = appOptions.Value;
|
||||
_serverOptions = serverOptions.Value;
|
||||
_updateOptions = updateOptions.Value;
|
||||
_logOptions = logOptions.Value;
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetConfigDictionary()
|
||||
@@ -144,7 +161,7 @@ namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
const string defaultValue = "*";
|
||||
|
||||
var bindAddress = GetValue("BindAddress", defaultValue);
|
||||
var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue);
|
||||
if (string.IsNullOrWhiteSpace(bindAddress))
|
||||
{
|
||||
return defaultValue;
|
||||
@@ -154,19 +171,19 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public int Port => GetValueInt("Port", DEFAULT_PORT);
|
||||
public int Port => _serverOptions.Port ?? GetValueInt("Port", DEFAULT_PORT);
|
||||
|
||||
public int SslPort => GetValueInt("SslPort", DEFAULT_SSL_PORT);
|
||||
public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", DEFAULT_SSL_PORT);
|
||||
|
||||
public bool EnableSsl => GetValueBoolean("EnableSsl", false);
|
||||
public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false);
|
||||
|
||||
public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true);
|
||||
public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true);
|
||||
|
||||
public string ApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
var apiKey = GetValue("ApiKey", GenerateApiKey());
|
||||
var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey());
|
||||
|
||||
if (apiKey.IsNullOrWhiteSpace())
|
||||
{
|
||||
@@ -182,7 +199,7 @@ namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
var enabled = GetValueBoolean("AuthenticationEnabled", false, false);
|
||||
var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false);
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
@@ -190,37 +207,45 @@ namespace NzbDrone.Core.Configuration
|
||||
return AuthenticationType.Basic;
|
||||
}
|
||||
|
||||
return GetValueEnum("AuthenticationMethod", AuthenticationType.None);
|
||||
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("AuthenticationMethod", AuthenticationType.None);
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||
public AuthenticationRequiredType AuthenticationRequired =>
|
||||
Enum.TryParse<AuthenticationRequiredType>(_authOptions.Required, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||
|
||||
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||
public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||
|
||||
// TODO: Change back to "master" for the first stable release.
|
||||
public string Branch => GetValue("Branch", "master").ToLowerInvariant();
|
||||
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "master").ToLowerInvariant();
|
||||
|
||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
|
||||
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
|
||||
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
|
||||
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
|
||||
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
|
||||
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
|
||||
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
|
||||
public string Theme => GetValue("Theme", "auto", persist: false);
|
||||
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
||||
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
public string SslCertPath => GetValue("SslCertPath", "");
|
||||
public string SslCertPassword => GetValue("SslCertPassword", "");
|
||||
|
||||
public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false);
|
||||
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
|
||||
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
|
||||
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
||||
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
||||
|
||||
public string UrlBase
|
||||
{
|
||||
get
|
||||
{
|
||||
var urlBase = GetValue("UrlBase", "").Trim('/');
|
||||
var urlBase = _serverOptions.UrlBase ?? GetValue("UrlBase", "").Trim('/');
|
||||
|
||||
if (urlBase.IsNullOrWhiteSpace())
|
||||
{
|
||||
@@ -232,19 +257,36 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
|
||||
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
|
||||
public string InstanceName => GetValue("InstanceName", BuildInfo.AppName);
|
||||
|
||||
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
|
||||
public string InstanceName
|
||||
{
|
||||
get
|
||||
{
|
||||
var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
|
||||
|
||||
public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
|
||||
if (instanceName.ContainsIgnoreCase(BuildInfo.AppName))
|
||||
{
|
||||
return instanceName;
|
||||
}
|
||||
|
||||
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
|
||||
return BuildInfo.AppName;
|
||||
}
|
||||
}
|
||||
|
||||
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
|
||||
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false);
|
||||
|
||||
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
|
||||
public UpdateMechanism UpdateMechanism =>
|
||||
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
|
||||
|
||||
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, false).ToLowerInvariant();
|
||||
public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false);
|
||||
|
||||
public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false);
|
||||
|
||||
public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false);
|
||||
|
||||
public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
|
||||
|
||||
public int GetValueInt(string key, int defaultValue, bool persist = true)
|
||||
{
|
||||
@@ -277,13 +319,13 @@ namespace NzbDrone.Core.Configuration
|
||||
return valueHolder.First().Value.Trim();
|
||||
}
|
||||
|
||||
//Save the value
|
||||
// Save the value
|
||||
if (persist)
|
||||
{
|
||||
SetValue(key, defaultValue);
|
||||
}
|
||||
|
||||
//return the default value
|
||||
// return the default value
|
||||
return defaultValue.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Datastore
|
||||
}
|
||||
|
||||
public CorruptDatabaseException(string message, Exception innerException, params object[] args)
|
||||
: base(message, innerException, args)
|
||||
: base(innerException, message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public CorruptDatabaseException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
: base(innerException, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace NzbDrone.Core.Datastore.Extensions
|
||||
public static IContainer AddDatabase(this IContainer container)
|
||||
{
|
||||
container.RegisterDelegate<IDbFactory, IMainDatabase>(f => new MainDatabase(f.Create()), Reuse.Singleton);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public static IContainer AddLogDatabase(this IContainer container)
|
||||
{
|
||||
container.RegisterDelegate<IDbFactory, ILogDatabase>(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton);
|
||||
|
||||
return container;
|
||||
@@ -16,6 +22,12 @@ namespace NzbDrone.Core.Datastore.Extensions
|
||||
public static IContainer AddDummyDatabase(this IContainer container)
|
||||
{
|
||||
container.RegisterInstance<IMainDatabase>(new MainDatabase(null));
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public static IContainer AddDummyLogDatabase(this IContainer container)
|
||||
{
|
||||
container.RegisterInstance<ILogDatabase>(new LogDatabase(null));
|
||||
|
||||
return container;
|
||||
|
||||
@@ -190,16 +190,16 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode)));
|
||||
_logger.Error("Proxy validation failed: {0}", response.StatusCode);
|
||||
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary<string, object> { { "statusCode", response.StatusCode } })));
|
||||
}
|
||||
|
||||
var result = JsonConvert.DeserializeObject<FlareSolverrResponse>(response.Content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url.Host)));
|
||||
_logger.Error(ex, "Proxy validation failed");
|
||||
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary<string, object> { { "exceptionMessage", ex.Message } })));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
|
||||
@@ -41,14 +41,14 @@ namespace NzbDrone.Core.IndexerProxies
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy. StatusCode: {0}", response.StatusCode)));
|
||||
_logger.Error("Proxy validation failed: {0}", response.StatusCode);
|
||||
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary<string, object> { { "statusCode", response.StatusCode } })));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", ex.Message)));
|
||||
_logger.Error(ex, "Proxy validation failed");
|
||||
failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary<string, object> { { "exceptionMessage", ex.Message } })));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
@@ -117,7 +117,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
else if (searchCriteria.Season > 0 && int.TryParse(searchCriteria.Episode, out var episode) && episode > 0)
|
||||
{
|
||||
// Standard (S/E) Episode
|
||||
parameters.Name = $"S{searchCriteria.Season:00}E{episode:00}";
|
||||
parameters.Name = $"S{searchCriteria.Season:00}E{episode:00}%";
|
||||
parameters.Category = "Episode";
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.IndexerVersions;
|
||||
@@ -47,7 +49,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
var generator = _generatorCache.Get(Settings.DefinitionFile, () =>
|
||||
var cacheKey = $"{Settings.DefinitionFile}.{HashUtil.ComputeSha256Hash(Settings.ToJson())}";
|
||||
var generator = _generatorCache.Get(cacheKey, () =>
|
||||
new CardigannRequestGenerator(_configService,
|
||||
_definitionService.GetCachedDefinition(Settings.DefinitionFile),
|
||||
_logger,
|
||||
|
||||
@@ -114,6 +114,24 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
// Amstrad
|
||||
caps.Categories.AddCategoryMapping("Amstrad CPC", NewznabStandardCategory.ConsoleOther, "Amstrad CPC");
|
||||
|
||||
// Bandai
|
||||
caps.Categories.AddCategoryMapping("Bandai WonderSwan", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan");
|
||||
caps.Categories.AddCategoryMapping("Bandai WonderSwan Color", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan Color");
|
||||
|
||||
// Commodore
|
||||
caps.Categories.AddCategoryMapping("Commodore 64", NewznabStandardCategory.ConsoleOther, "Commodore 64");
|
||||
caps.Categories.AddCategoryMapping("Commodore 128", NewznabStandardCategory.ConsoleOther, "Commodore 128");
|
||||
caps.Categories.AddCategoryMapping("Commodore Amiga", NewznabStandardCategory.ConsoleOther, "Commodore Amiga");
|
||||
caps.Categories.AddCategoryMapping("Amiga CD32", NewznabStandardCategory.ConsoleOther, "Amiga CD32");
|
||||
caps.Categories.AddCategoryMapping("Commodore Plus-4", NewznabStandardCategory.ConsoleOther, "Commodore Plus-4");
|
||||
caps.Categories.AddCategoryMapping("Commodore VIC-20", NewznabStandardCategory.ConsoleOther, "Commodore VIC-20");
|
||||
|
||||
// NEC
|
||||
caps.Categories.AddCategoryMapping("NEC PC-98", NewznabStandardCategory.ConsoleOther, "NEC PC-98");
|
||||
caps.Categories.AddCategoryMapping("NEC PC-FX", NewznabStandardCategory.ConsoleOther, "NEC PC-FX");
|
||||
caps.Categories.AddCategoryMapping("NEC SuperGrafx", NewznabStandardCategory.ConsoleOther, "NEC SuperGrafx");
|
||||
caps.Categories.AddCategoryMapping("NEC TurboGrafx-16", NewznabStandardCategory.ConsoleOther, "NEC TurboGrafx-16");
|
||||
|
||||
// Sinclair
|
||||
caps.Categories.AddCategoryMapping("ZX Spectrum", NewznabStandardCategory.ConsoleOther, "ZX Spectrum");
|
||||
|
||||
@@ -137,16 +155,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
// Other
|
||||
caps.Categories.AddCategoryMapping("3DO", NewznabStandardCategory.ConsoleOther, "3DO");
|
||||
caps.Categories.AddCategoryMapping("Bandai WonderSwan", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan");
|
||||
caps.Categories.AddCategoryMapping("Bandai WonderSwan Color", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan Color");
|
||||
caps.Categories.AddCategoryMapping("Casio Loopy", NewznabStandardCategory.ConsoleOther, "Casio Loopy");
|
||||
caps.Categories.AddCategoryMapping("Casio PV-1000", NewznabStandardCategory.ConsoleOther, "Casio PV-1000");
|
||||
caps.Categories.AddCategoryMapping("Colecovision", NewznabStandardCategory.ConsoleOther, "Colecovision");
|
||||
caps.Categories.AddCategoryMapping("Commodore 64", NewznabStandardCategory.ConsoleOther, "Commodore 64");
|
||||
caps.Categories.AddCategoryMapping("Commodore 128", NewznabStandardCategory.ConsoleOther, "Commodore 128");
|
||||
caps.Categories.AddCategoryMapping("Commodore Amiga", NewznabStandardCategory.ConsoleOther, "Commodore Amiga");
|
||||
caps.Categories.AddCategoryMapping("Commodore Plus-4", NewznabStandardCategory.ConsoleOther, "Commodore Plus-4");
|
||||
caps.Categories.AddCategoryMapping("Commodore VIC-20", NewznabStandardCategory.ConsoleOther, "Commodore VIC-20");
|
||||
caps.Categories.AddCategoryMapping("Emerson Arcadia 2001", NewznabStandardCategory.ConsoleOther, "Emerson Arcadia 2001");
|
||||
caps.Categories.AddCategoryMapping("Entex Adventure Vision", NewznabStandardCategory.ConsoleOther, "Entex Adventure Vision");
|
||||
caps.Categories.AddCategoryMapping("Epoch Super Casette Vision", NewznabStandardCategory.ConsoleOther, "Epoch Super Casette Vision");
|
||||
@@ -161,13 +172,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping("Mattel Intellivision", NewznabStandardCategory.ConsoleOther, "Mattel Intellivision");
|
||||
caps.Categories.AddCategoryMapping("Memotech MTX", NewznabStandardCategory.ConsoleOther, "Memotech MTX");
|
||||
caps.Categories.AddCategoryMapping("Miles Gordon Sam Coupe", NewznabStandardCategory.ConsoleOther, "Miles Gordon Sam Coupe");
|
||||
caps.Categories.AddCategoryMapping("NEC PC-98", NewznabStandardCategory.ConsoleOther, "NEC PC-98");
|
||||
caps.Categories.AddCategoryMapping("NEC PC-FX", NewznabStandardCategory.ConsoleOther, "NEC PC-FX");
|
||||
caps.Categories.AddCategoryMapping("NEC SuperGrafx", NewznabStandardCategory.ConsoleOther, "NEC SuperGrafx");
|
||||
caps.Categories.AddCategoryMapping("NEC TurboGrafx-16", NewznabStandardCategory.ConsoleOther, "NEC TurboGrafx-16");
|
||||
caps.Categories.AddCategoryMapping("Nokia N-Gage", NewznabStandardCategory.ConsoleOther, "Nokia N-Gage");
|
||||
caps.Categories.AddCategoryMapping("Oculus Quest", NewznabStandardCategory.ConsoleOther, "Oculus Quest");
|
||||
caps.Categories.AddCategoryMapping("Ouya", NewznabStandardCategory.ConsoleOther, "Ouya");
|
||||
caps.Categories.AddCategoryMapping("Philips Videopac+", NewznabStandardCategory.ConsoleOther, "Philips Videopac+");
|
||||
caps.Categories.AddCategoryMapping("Philips CD-i", NewznabStandardCategory.ConsoleOther, "Philips CD-i");
|
||||
caps.Categories.AddCategoryMapping("Phone/PDA", NewznabStandardCategory.ConsoleOther, "Phone/PDA");
|
||||
caps.Categories.AddCategoryMapping("RCA Studio II", NewznabStandardCategory.ConsoleOther, "RCA Studio II");
|
||||
caps.Categories.AddCategoryMapping("Sharp X1", NewznabStandardCategory.ConsoleOther, "Sharp X1");
|
||||
@@ -324,6 +333,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
categoryMappings.ForEach(category => parameters.Add("artistcheck[]", category));
|
||||
}
|
||||
|
||||
if (_settings.FreeleechOnly)
|
||||
{
|
||||
parameters.Add("freetorrent", "1");
|
||||
}
|
||||
|
||||
if (searchCriteria.MinSize is > 0)
|
||||
{
|
||||
var minSize = searchCriteria.MinSize.Value / 1024L / 1024L;
|
||||
@@ -404,6 +418,15 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
Enum.TryParse(torrent.Value.FreeTorrent, true, out GazelleGamesFreeTorrent freeTorrent);
|
||||
var downloadVolumeFactor = freeTorrent is GazelleGamesFreeTorrent.FreeLeech or GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1;
|
||||
|
||||
// Skip non-freeleech results when freeleech only is set
|
||||
if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var torrentId = torrent.Key;
|
||||
var infoUrl = GetInfoUrl(group.Key, torrentId);
|
||||
|
||||
@@ -412,8 +435,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
categories = _categories.MapTrackerCatToNewznab(torrent.Value.CategoryId.ToString()).ToArray();
|
||||
}
|
||||
|
||||
Enum.TryParse(torrent.Value.FreeTorrent, true, out GazelleGamesFreeTorrent freeTorrent);
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
@@ -428,7 +449,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Peers = torrent.Value.Leechers + torrent.Value.Seeders,
|
||||
PublishDate = torrent.Value.Time.ToUniversalTime(),
|
||||
Scene = torrent.Value.Scene == 1,
|
||||
DownloadVolumeFactor = freeTorrent is GazelleGamesFreeTorrent.FreeLeech or GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1,
|
||||
DownloadVolumeFactor = downloadVolumeFactor,
|
||||
UploadVolumeFactor = freeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1,
|
||||
MinimumSeedTime = 288000 // Minimum of 3 days and 8 hours (80 hours in total)
|
||||
};
|
||||
@@ -543,6 +564,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
[FieldDefinition(3, Label = "IndexerGazelleGamesSettingsSearchGroupNames", Type = FieldType.Checkbox, HelpText = "IndexerGazelleGamesSettingsSearchGroupNamesHelpText")]
|
||||
public bool SearchGroupNames { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", HelpText = "IndexerGazelleGamesSettingsFreeleechOnlyHelpText", Type = FieldType.Checkbox, Advanced = true)]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
public string Passkey { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -525,7 +525,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
[FieldDefinition(3, Type = FieldType.Select, Label = "Search Type", SelectOptions = typeof(MyAnonamouseSearchType), HelpText = "Specify the desired search type")]
|
||||
public int SearchType { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Buy Freeleech Token", HelpText = "Buy personal freeleech token for download")]
|
||||
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Use Freeleech Wedges", HelpText = "Use freeleech wedges to make grabbed torrents personal freeleech")]
|
||||
public bool Freeleech { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "Search in description", HelpText = "Search text in the description")]
|
||||
|
||||
@@ -20,16 +20,21 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public override string Name => "TorrentDay";
|
||||
public override string[] IndexerUrls => new[]
|
||||
{
|
||||
"https://torrentday.cool/",
|
||||
"https://tday.love/",
|
||||
"https://torrentday.cool/",
|
||||
"https://secure.torrentday.com/",
|
||||
"https://classic.torrentday.com/",
|
||||
"https://www.torrentday.com/",
|
||||
"https://www.torrentday.me/",
|
||||
"https://torrentday.it/",
|
||||
"https://td.findnemo.net/",
|
||||
"https://td.getcrazy.me/",
|
||||
"https://td.venom.global/",
|
||||
"https://td.workisboring.net/"
|
||||
"https://td.workisboring.net/",
|
||||
"https://tday.findnemo.net/",
|
||||
"https://tday.getcrazy.me/",
|
||||
"https://tday.venom.global/",
|
||||
"https://tday.workisboring.net/"
|
||||
};
|
||||
public override string Description => "TorrentDay (TD) is a Private site for TV / MOVIES / GENERAL";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
@@ -266,7 +271,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
DownloadVolumeFactor = downloadMultiplier,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800 // 48 hours
|
||||
MinimumSeedTime = 259200 // 72 hours
|
||||
};
|
||||
|
||||
torrentInfos.Add(release);
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Indexers
|
||||
where TSettings : IIndexerSettings, new()
|
||||
{
|
||||
protected const int MaxNumResultsPerQuery = 1000;
|
||||
private const int MaxRedirects = 5;
|
||||
|
||||
protected readonly IIndexerHttpClient _httpClient;
|
||||
protected readonly IEventAggregator _eventAggregator;
|
||||
@@ -38,6 +39,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
{ Result.HasHttpServerError: true } => PredicateResult.True(),
|
||||
{ Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(),
|
||||
{ Exception: HttpException { Response.HasHttpServerError: true } } => PredicateResult.True(),
|
||||
_ => PredicateResult.False()
|
||||
},
|
||||
Delay = RateLimit,
|
||||
@@ -238,11 +240,47 @@ namespace NzbDrone.Core.Indexers
|
||||
request.RateLimit = RateLimit;
|
||||
}
|
||||
|
||||
request.AllowAutoRedirect = false;
|
||||
|
||||
byte[] fileData;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
|
||||
if (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.SeeOther)
|
||||
{
|
||||
var autoRedirectChain = new List<string> { request.Url.ToString() };
|
||||
|
||||
do
|
||||
{
|
||||
var redirectUrl = response.RedirectUrl;
|
||||
|
||||
_logger.Debug("Download request is being redirected to: {0}", redirectUrl);
|
||||
|
||||
if (redirectUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new WebException("Remote website tried to redirect without providing a location.");
|
||||
}
|
||||
|
||||
if (redirectUrl.StartsWith("magnet:"))
|
||||
{
|
||||
return await Download(new Uri(redirectUrl));
|
||||
}
|
||||
|
||||
request.Url = new HttpUri(redirectUrl);
|
||||
autoRedirectChain.Add(request.Url.ToString());
|
||||
|
||||
if (autoRedirectChain.Count > MaxRedirects)
|
||||
{
|
||||
throw new WebException($"Too many download redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
|
||||
}
|
||||
|
||||
response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
}
|
||||
while (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.SeeOther);
|
||||
}
|
||||
|
||||
fileData = response.ResponseData;
|
||||
|
||||
_logger.Debug("Downloaded for release finished ({0} bytes from {1})", fileData.Length, link.AbsoluteUri);
|
||||
@@ -286,10 +324,7 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
protected virtual Task<HttpRequest> GetDownloadRequest(Uri link)
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri)
|
||||
{
|
||||
AllowAutoRedirect = FollowRedirect
|
||||
};
|
||||
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri);
|
||||
|
||||
if (Cookies != null)
|
||||
{
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Indexers
|
||||
[FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsAppsMinimumSeeders", HelpText = "IndexerSettingsAppsMinimumSeedersHelpText", Advanced = true)]
|
||||
public int? AppMinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Textbox, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")]
|
||||
[FieldDefinition(2, Type = FieldType.Number, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")]
|
||||
public double? SeedRatio { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)]
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"ScriptPath": "Camí de l'script",
|
||||
"Search": "Cerca",
|
||||
"Files": "Fitxers",
|
||||
"SettingsEnableColorImpairedModeHelpText": "Estil alternat per a permetre als usuaris amb problemes de color distingir millor la informació codificada per colors",
|
||||
"SettingsEnableColorImpairedModeHelpText": "Estil modificat per a permetre als usuaris amb problemes visuals distingir millor la informació codificada per colors",
|
||||
"TagIsNotUsedAndCanBeDeleted": "L'etiqueta no està en ús i es pot suprimir",
|
||||
"TagsSettingsSummary": "Consulta totes les etiquetes i com s'utilitzen. Les etiquetes no utilitzades es poden eliminar",
|
||||
"Tasks": "Tasques",
|
||||
@@ -196,7 +196,7 @@
|
||||
"MovieIndexScrollBottom": "Índex de pel·lícules: Desplaçament inferior",
|
||||
"OnApplicationUpdate": "A l'actualitzar de l'aplicació",
|
||||
"OnApplicationUpdateHelpText": "A l'actualitzar de l'aplicació",
|
||||
"OnGrab": "Al capturar",
|
||||
"OnGrab": "Al capturar llançament",
|
||||
"PackageVersion": "Versió del paquet",
|
||||
"ProxyFailedToTestHealthCheckMessage": "No s'ha pogut provar el servidor intermediari: {url}",
|
||||
"ProxyResolveIpHealthCheckMessage": "No s'ha pogut resoldre l'adreça IP de l'amfitrió intermediari configurat {proxyHostName}",
|
||||
@@ -240,7 +240,7 @@
|
||||
"IndexerPriority": "Prioritat de l'indexador",
|
||||
"UnsavedChanges": "Canvis no desats",
|
||||
"UpdateAutomaticallyHelpText": "Baixeu i instal·leu les actualitzacions automàticament. Encara podreu instal·lar des de Sistema: Actualitzacions",
|
||||
"UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici \"{startupFolder}\" es troba en una carpeta de translocació d'aplicacions.",
|
||||
"UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici '{startupFolder}' es troba en una carpeta de translocació d'aplicacions.",
|
||||
"UpdateUiNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no pot escriure la carpeta de la IU '{uiFolder}'.",
|
||||
"UpdateScriptPathHelpText": "Camí a un script personalitzat que pren un paquet d'actualització i gestiona la resta del procés d'actualització",
|
||||
"Uptime": "Temps de funcionament",
|
||||
@@ -294,62 +294,62 @@
|
||||
"UnableToAddANewNotificationPleaseTryAgain": "No es pot afegir una notificació nova, torneu-ho a provar.",
|
||||
"UnableToLoadGeneralSettings": "No es pot carregar la configuració general",
|
||||
"UnableToLoadHistory": "No es pot carregar l'historial",
|
||||
"UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari \"{userName}\" no pot escriure la carpeta d'inici \"{startupFolder}\".",
|
||||
"UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no té permisos d'escriptura de la carpeta d'inici '{startupFolder}'.",
|
||||
"URLBase": "Base URL",
|
||||
"Usenet": "Usenet",
|
||||
"View": "Visualitza",
|
||||
"Yesterday": "Ahir",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Llistes no disponibles a causa d'errors: {0}",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Aplicacions no disponibles a causa d'errors: {0}",
|
||||
"AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de {appName}. Això inclou informació sobre el vostre navegador, quines pàgines {appName} WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió de l'entorn d'execució. Utilitzarem aquesta informació per a prioritzar les funcions i les correccions d'errors.",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Els fitxers de la paperera de reciclatge més antics que el nombre de dies seleccionat es netejaran automàticament",
|
||||
"UnableToAddANewAppProfilePleaseTryAgain": "No es pot afegir un perfil de qualitat nou, torneu-ho a provar.",
|
||||
"UnableToAddANewAppProfilePleaseTryAgain": "No es pot afegir un perfil d'aplicació nou, torneu-ho a provar.",
|
||||
"BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData de {appName}",
|
||||
"AllIndexersHiddenDueToFilter": "Totes les pel·lícules estan ocultes a causa del filtre aplicat.",
|
||||
"AllIndexersHiddenDueToFilter": "Tots els indexadors estan ocults a causa del filtre aplicat.",
|
||||
"EnableRss": "Activa RSS",
|
||||
"Grabs": "Captura",
|
||||
"EnableAutomaticSearchHelpText": "S'utilitzarà quan es realitzin cerques automàtiques mitjançant la interfície d'usuari o per {appName}",
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "No es pot afegir una notificació nova, torneu-ho a provar.",
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "No es pot afegir una aplicació nova, torneu-ho a provar.",
|
||||
"Application": "Aplicacions",
|
||||
"Applications": "Aplicacions",
|
||||
"ApplicationStatusCheckAllClientMessage": "Totes les llistes no estan disponibles a causa d'errors",
|
||||
"ApplicationStatusCheckAllClientMessage": "Totes les aplicacions no estan disponibles a causa d'errors",
|
||||
"AuthenticationMethodHelpText": "Es requereix nom d'usuari i contrasenya per a accedir a {appName}",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors durant més de 6 hores",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors durant més de 6 hores: {0}",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Totes les aplicacions no estan disponibles a causa d'errors durant més de 6 hores",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Aplicacions no disponibles a causa d'errors durant més de 6 hores: {0}",
|
||||
"BindAddressHelpText": "Adreça IP vàlida, localhost o '*' per a totes les interfícies",
|
||||
"BranchUpdate": "Branca que s'utilitza per a actualitzar {appName}",
|
||||
"Connect": "Notificacions",
|
||||
"DeleteApplicationMessageText": "Esteu segur que voleu suprimir la notificació '{0}'?",
|
||||
"DeleteIndexerProxyMessageText": "Esteu segur que voleu suprimir la llista '{0}'?",
|
||||
"DeleteApplicationMessageText": "Esteu segur que voleu suprimir l'aplicació '{name}'?",
|
||||
"DeleteIndexerProxyMessageText": "Esteu segur que voleu suprimir el servidor intermediari de l'indexador '{name}'?",
|
||||
"Encoding": "Codificació",
|
||||
"ForMoreInformationOnTheIndividualDownloadClients": "Per obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.",
|
||||
"ForMoreInformationOnTheIndividualDownloadClients": "Per a obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.",
|
||||
"GeneralSettingsSummary": "Port, SSL, nom d'usuari/contrasenya, servidor intermediari, analítiques i actualitzacions",
|
||||
"GrabReleases": "Captura novetat",
|
||||
"GrabReleases": "Captura llançament(s)",
|
||||
"HistoryCleanupDaysHelpText": "Establiu a 0 per desactivar la neteja automàtica",
|
||||
"Notification": "Notificacions",
|
||||
"Notifications": "Notificacions",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "La branca {0} no és una branca de llançament de {appName} vàlida, no rebreu actualitzacions",
|
||||
"TagsHelpText": "S'aplica a pel·lícules amb almenys una etiqueta coincident",
|
||||
"Torrent": "Torrent",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "No es pot afegir un indexador nou, torneu-ho a provar.",
|
||||
"TagsHelpText": "S'aplica a indexadors amb almenys una etiqueta coincident",
|
||||
"Torrent": "Torrents",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "No es pot afegir un servidor intermediari d'indexador nou, torneu-ho a provar.",
|
||||
"UpdateMechanismHelpText": "Utilitzeu l'actualitzador integrat de {appName} o un script",
|
||||
"UserAgentProvidedByTheAppThatCalledTheAPI": "Agent d'usuari proporcionat per l'aplicació per fer peticions a l'API",
|
||||
"IndexerProxyStatusAllUnavailableHealthCheckMessage": "Tots els indexadors no estan disponibles a causa d'errors",
|
||||
"IndexerProxyStatusUnavailableHealthCheckMessage": "Els indexadors no estan disponibles a causa d'errors: {indexerProxyNames}",
|
||||
"LaunchBrowserHelpText": " Obriu un navegador web i navegueu a la pàgina d'inici de {appName} a l'inici de l'aplicació.",
|
||||
"Link": "Enllaços",
|
||||
"Link": "Enllaç",
|
||||
"UILanguageHelpText": "Idioma que utilitzarà {appName} per a la interfície d'usuari",
|
||||
"Remove": "Elimina",
|
||||
"Replace": "Substitueix",
|
||||
"TheLatestVersionIsAlreadyInstalled": "La darrera versió de {0} 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 Theme.Park",
|
||||
"TheLatestVersionIsAlreadyInstalled": "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",
|
||||
"ApplicationUrlHelpText": "URL extern d'aquesta aplicació, inclòs http(s)://, port i URL base",
|
||||
"ApplyTagsHelpTextAdd": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent",
|
||||
"ApplyTagsHelpTextHowToApplyApplications": "Com aplicar etiquetes a les pel·lícules seleccionades",
|
||||
"ApplicationUrlHelpText": "URL extern de l'aplicació, inclòs http(s)://, port i URL base",
|
||||
"ApplyTagsHelpTextAdd": "Afegiment: afegeix les etiquetes a la llista d'etiquetes existent",
|
||||
"ApplyTagsHelpTextHowToApplyApplications": "Com aplicar etiquetes a les aplicacions seleccionades",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Com aplicar etiquetes als indexadors seleccionats",
|
||||
"ApplyTagsHelpTextRemove": "Eliminar: elimina les etiquetes introduïdes",
|
||||
"DeleteSelectedApplicationsMessageText": "Esteu segur que voleu suprimir l'indexador '{0}'?",
|
||||
"ApplyTagsHelpTextRemove": "Eliminació: elimina les etiquetes introduïdes",
|
||||
"DeleteSelectedApplicationsMessageText": "Esteu segur que voleu suprimir {count} sol·licituds seleccionades?",
|
||||
"Label": "Etiqueta",
|
||||
"ApplyTagsHelpTextReplace": "Substitució: substituïu les etiquetes per les etiquetes introduïdes (no introduïu cap etiqueta per a esborrar totes les etiquetes)",
|
||||
"DeleteSelectedDownloadClients": "Suprimeix el(s) client(s) de baixada",
|
||||
@@ -360,23 +360,23 @@
|
||||
"More": "Més",
|
||||
"Season": "Temporada",
|
||||
"Theme": "Tema",
|
||||
"Track": "Traça",
|
||||
"Track": "Pista",
|
||||
"Year": "Any",
|
||||
"UpdateAvailableHealthCheckMessage": "Nova actualització disponible",
|
||||
"ConnectionLostReconnect": "{appName} intentarà connectar-se automàticament, o podeu fer clic a recarregar.",
|
||||
"ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per a restaurar la funcionalitat.",
|
||||
"RecentChanges": "Canvis recents",
|
||||
"WhatsNew": "Que hi ha de nou?",
|
||||
"minutes": "Minuts",
|
||||
"DeleteAppProfileMessageText": "Esteu segur que voleu suprimir el perfil de qualitat {0}",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Llistes no disponibles a causa d'errors: {notificationNames}",
|
||||
"WhatsNew": "Novetats",
|
||||
"minutes": "minuts",
|
||||
"DeleteAppProfileMessageText": "Esteu segur que voleu suprimir el perfil de l'aplicació '{name}'?",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Notificacions no disponibles a causa d'errors: {notificationNames}",
|
||||
"AddConnection": "Afegeix una connexió",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "Totes les notificacions no estan disponibles a causa d'errors",
|
||||
"AuthBasic": "Basic (finestra emergent del navegador)",
|
||||
"AuthBasic": "Bàsic (finestra emergent del navegador)",
|
||||
"AuthForm": "Formularis (pàgina d'inici de sessió)",
|
||||
"DisabledForLocalAddresses": "Desactivat per a adreces locals",
|
||||
"None": "Cap",
|
||||
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau de l'API?",
|
||||
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau API?",
|
||||
"RestartProwlarr": "Reinicia {appName}",
|
||||
"AuthenticationRequired": "Autenticació necessària",
|
||||
"CountDownloadClientsSelected": "{count} client(s) de baixada seleccionat(s)",
|
||||
@@ -412,14 +412,14 @@
|
||||
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmeu la nova contrasenya",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Introduïu un nom d'usuari nou",
|
||||
"Categories": "Categories",
|
||||
"ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau de l'API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració",
|
||||
"ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració",
|
||||
"Episode": "Episodi",
|
||||
"EditApplicationImplementation": "Edita la notificació - {implementationName}",
|
||||
"EditApplicationImplementation": "Edita l'aplicació - {implementationName}",
|
||||
"EditConnectionImplementation": "Afegeix una connexió - {implementationName}",
|
||||
"EditIndexerProxyImplementation": "Edita l'indexador - {implementationName}",
|
||||
"EditIndexerProxyImplementation": "Edita el servidor intermediari de l'indexador - {implementationName}",
|
||||
"days": "dies",
|
||||
"Album": "àlbum",
|
||||
"Artist": "artista",
|
||||
"Album": "Àlbum",
|
||||
"Artist": "Artista",
|
||||
"AddApplicationImplementation": "Afegeix una condició - {implementationName}",
|
||||
"AddIndexerProxyImplementation": "Afegeix un indexador - {implementationName}",
|
||||
"Category": "Categoria",
|
||||
@@ -431,7 +431,7 @@
|
||||
"Id": "ID",
|
||||
"Author": "Autor",
|
||||
"ManageClients": "Gestiona els clients",
|
||||
"CountApplicationsSelected": "{count} Col·lecció(ns) seleccionades",
|
||||
"CountApplicationsSelected": "{count} aplicacions seleccionades",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2",
|
||||
"Donate": "Dona",
|
||||
"BlackholeFolderHelpText": "Carpeta on {appName} emmagatzemarà els fitxers {extension}",
|
||||
@@ -439,9 +439,9 @@
|
||||
"Directory": "Directori",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Afegeix un prefix a l'url json del Deluge, vegeu {url}",
|
||||
"CustomFilter": "Filtres personalitzats",
|
||||
"IndexerHDBitsSettingsCodecs": "Còdec",
|
||||
"DownloadClientSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {connectionName}, com ara {url}",
|
||||
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2",
|
||||
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"IndexerHDBitsSettingsMediums": "Suport"
|
||||
"IndexerHDBitsSettingsCodecs": "Còdecs",
|
||||
"DownloadClientSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {clientName}, com ara {url}",
|
||||
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Transmission",
|
||||
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de rTorrent",
|
||||
"IndexerHDBitsSettingsMediums": "Mitjans"
|
||||
}
|
||||
|
||||
@@ -89,15 +89,15 @@
|
||||
"DeleteApplication": "Applikation löschen",
|
||||
"DeleteApplicationMessageText": "Wirklich die Applikation '{0}' löschen?",
|
||||
"DeleteBackup": "Sicherung löschen",
|
||||
"DeleteBackupMessageText": "Sind Sie sicher, dass Sie die Sicherung „{name}“ löschen möchten?",
|
||||
"DeleteBackupMessageText": "Soll das Backup '{name}' wirklich gelöscht werden?",
|
||||
"DeleteDownloadClient": "Download-Client löschen",
|
||||
"DeleteDownloadClientMessageText": "Sind Sie sicher, dass Sie den Download-Client „{name}“ löschen möchten?",
|
||||
"DeleteDownloadClientMessageText": "Bist du sicher, dass du den Download Client '{name}' wirklich löschen willst?",
|
||||
"DeleteIndexerProxy": "Indexer Proxy löschen",
|
||||
"DeleteIndexerProxyMessageText": "Tag '{0}' wirklich löschen?",
|
||||
"DeleteNotification": "Benachrichtigung löschen",
|
||||
"DeleteNotificationMessageText": "Sind Sie sicher, dass Sie die Benachrichtigung „{name}“ löschen möchten?",
|
||||
"DeleteNotificationMessageText": "Bist du sicher, dass du die Benachrichtigung '{name}' wirklich löschen willst?",
|
||||
"DeleteTag": "Tag löschen",
|
||||
"DeleteTagMessageText": "Sind Sie sicher, dass Sie das Tag „{label}“ löschen möchten?",
|
||||
"DeleteTagMessageText": "Bist du sicher, dass du den Tag '{label}' wirklich löschen willst?",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Einzelheiten",
|
||||
"DevelopmentSettings": "Entwicklungseinstellungen",
|
||||
@@ -216,7 +216,7 @@
|
||||
"Logging": "Protokollierung",
|
||||
"Logs": "Protokolle",
|
||||
"MIA": "MIA",
|
||||
"MaintenanceRelease": "Wartung: Fehlerbehebung und andere Verbesserungen. Siehe Github Commit History für weitere Details",
|
||||
"MaintenanceRelease": "Maintenance Release: Fehlerbehebungen und andere Verbesserungen. Siehe Github Commit Verlauf für weitere Details",
|
||||
"Manual": "Manuell",
|
||||
"MappedDrivesRunningAsService": "Zugeordnete Netzlaufwerke sind nicht verfügbar, wenn {appName} als Windows-Dienst ausgeführt wird. Bitte lesen Sie die FAQ für weitere Informationen",
|
||||
"MassEditor": "Masseneditor",
|
||||
@@ -471,11 +471,11 @@
|
||||
"NoIndexersFound": "Keine Indexer gefunden",
|
||||
"Theme": "Design",
|
||||
"Season": "Staffel",
|
||||
"ApplyTagsHelpTextAdd": "Hinzufügen: Fügen Sie die Tags der vorhandenen Tag-Liste hinzu",
|
||||
"ApplyTagsHelpTextAdd": "Hinzufügen: Füge Tags zu den bestehenden Tags hinzu",
|
||||
"ApplyTagsHelpTextHowToApplyApplications": "Wie werden Tags zu ausgewählten Autoren zugeteilt",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "So wenden Sie Tags auf die ausgewählten Indexer an",
|
||||
"ApplyTagsHelpTextRemove": "Entfernen: Die eingegebenen Tags entfernen",
|
||||
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetzen Sie die Tags durch die eingegebenen Tags (geben Sie keine Tags ein, um alle Tags zu löschen).",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Wie Tags zu den selektierten Indexern hinzugefügt werden können",
|
||||
"ApplyTagsHelpTextRemove": "Entfernen: Entferne die hinterlegten Tags",
|
||||
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetze die Tags mit den eingegebenen Tags (keine Tags eingeben um alle Tags zu löschen)",
|
||||
"DownloadClientPriorityHelpText": "Priorisiere mehrere Downloader. Rundlauf-Verfahren wird für Downloader mit der gleichen Priorität verwendet.",
|
||||
"EditSelectedIndexers": "Ausgewähle Indexer bearbeiten",
|
||||
"SelectIndexers": "Indexer suchen",
|
||||
@@ -488,7 +488,7 @@
|
||||
"UpdateAvailableHealthCheckMessage": "Neue Version verfügbar",
|
||||
"Year": "Jahr",
|
||||
"Album": "Album",
|
||||
"Artist": "Künstler",
|
||||
"Artist": "Interpret",
|
||||
"Author": "Autor",
|
||||
"Book": "Buch",
|
||||
"ConnectionLostReconnect": "{appName} wird versuchen, automatisch eine Verbindung herzustellen, oder Sie können unten auf „Neu laden“ klicken.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"DatabaseMigration": "Database Migration",
|
||||
"Date": "Date",
|
||||
"Dates": "Dates",
|
||||
"Default": "Default",
|
||||
"DefaultCategory": "Default Category",
|
||||
"DefaultNameCopiedProfile": "{name} - Copy",
|
||||
"Delete": "Delete",
|
||||
@@ -286,6 +287,7 @@
|
||||
"GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics, and updates",
|
||||
"Genre": "Genre",
|
||||
"GoToApplication": "Go to application",
|
||||
"GrabRelease": "Grab Release",
|
||||
"GrabReleases": "Grab Release(s)",
|
||||
"GrabTitle": "Grab Title",
|
||||
"Grabbed": "Grabbed",
|
||||
@@ -336,6 +338,7 @@
|
||||
"IndexerFlags": "Indexer Flags",
|
||||
"IndexerGazelleGamesSettingsApiKeyHelpText": "API Key from the Site (Found in Settings => Access Settings)",
|
||||
"IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Must have User and Torrents permissions",
|
||||
"IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Search freeleech releases only",
|
||||
"IndexerGazelleGamesSettingsSearchGroupNames": "Search Group Names",
|
||||
"IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Search releases by group names",
|
||||
"IndexerHDBitsSettingsCodecs": "Codecs",
|
||||
@@ -447,6 +450,7 @@
|
||||
"ManageClients": "Manage Clients",
|
||||
"ManageDownloadClients": "Manage Download Clients",
|
||||
"Manual": "Manual",
|
||||
"ManualGrab": "Manual Grab",
|
||||
"MappedCategories": "Mapped Categories",
|
||||
"MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information",
|
||||
"MassEditor": "Mass Editor",
|
||||
@@ -507,9 +511,12 @@
|
||||
"OnHealthIssueHelpText": "On Health Issue",
|
||||
"OnHealthRestored": "On Health Restored",
|
||||
"OnHealthRestoredHelpText": "On Health Restored",
|
||||
"Open": "Open",
|
||||
"OpenBrowserOnStart": "Open browser on start",
|
||||
"OpenThisModal": "Open This Modal",
|
||||
"Options": "Options",
|
||||
"OverrideAndAddToDownloadClient": "Override and add to download client",
|
||||
"OverrideGrabModalTitle": "Override and Grab - {title}",
|
||||
"PackSeedTime": "Pack Seed Time",
|
||||
"PackSeedTimeHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default",
|
||||
"PackageVersion": "Package Version",
|
||||
@@ -526,6 +533,7 @@
|
||||
"PortNumber": "Port Number",
|
||||
"Presets": "Presets",
|
||||
"Priority": "Priority",
|
||||
"PrioritySettings": "Priority: {priority}",
|
||||
"Privacy": "Privacy",
|
||||
"Private": "Private",
|
||||
"Protocol": "Protocol",
|
||||
@@ -542,6 +550,8 @@
|
||||
"ProxyResolveIpHealthCheckMessage": "Failed to resolve the IP Address for the Configured Proxy Host {proxyHostName}",
|
||||
"ProxyType": "Proxy Type",
|
||||
"ProxyUsernameHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.",
|
||||
"ProxyValidationBadRequest": "Failed to test proxy. Status code: {statusCode}",
|
||||
"ProxyValidationUnableToConnect": "Unable to connect to proxy: {exceptionMessage}. Check the log surrounding this error for details",
|
||||
"Public": "Public",
|
||||
"Publisher": "Publisher",
|
||||
"Query": "Query",
|
||||
@@ -609,6 +619,7 @@
|
||||
"SeedTimeHelpText": "The time a torrent should be seeded before stopping, empty is app's default",
|
||||
"Seeders": "Seeders",
|
||||
"SelectAll": "Select All",
|
||||
"SelectDownloadClientModalTitle": "{modalTitle} - Select Download Client",
|
||||
"SelectIndexers": "Select Indexers",
|
||||
"SelectedCountOfCountReleases": "Selected {selectedCount} of {itemCount} releases",
|
||||
"SemiPrivate": "Semi-Private",
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
"About": "Acerca de",
|
||||
"View": "Vista",
|
||||
"Updates": "Actualizaciones",
|
||||
"UpdateUiNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta UI '{uiFolder}' no tiene permisos de escritura para el usuario '{userName}'.",
|
||||
"UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' está en una carpeta de \"App Translocation\".",
|
||||
"UpdateUiNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de interfaz de usuario '{uiFolder}' no es modificable por el usuario '{userName}'.",
|
||||
"UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' está en una carpeta de translocación de aplicaciones.",
|
||||
"UpdateStartupNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' no tiene permisos de escritura para el usuario '{userName}'.",
|
||||
"UnselectAll": "Desmarcar todo",
|
||||
"UI": "UI",
|
||||
"UI": "Interfaz",
|
||||
"Tasks": "Tareas",
|
||||
"Tags": "Etiquetas",
|
||||
"System": "Sistema",
|
||||
@@ -164,10 +164,10 @@
|
||||
"UseProxy": "Usar proxy",
|
||||
"Usenet": "Usenet",
|
||||
"UrlBaseHelpText": "Para soporte de proxy inverso, por defecto está vacío",
|
||||
"URLBase": "URL Base",
|
||||
"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 incorporado de {appName} o un script",
|
||||
"UpdateMechanismHelpText": "Usar 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",
|
||||
@@ -214,7 +214,7 @@
|
||||
"LogLevel": "Nivel de Registro",
|
||||
"LaunchBrowserHelpText": " Abrir un navegador web e ir a la página de inicio de {appName} al arrancar la app.",
|
||||
"Interval": "Intervalo",
|
||||
"IndexerFlags": "Banderas del indexador",
|
||||
"IndexerFlags": "Indicadores del indexador",
|
||||
"IncludeHealthWarningsHelpText": "Incluir Alertas de Salud",
|
||||
"IllRestartLater": "Lo reiniciaré más tarde",
|
||||
"IgnoredAddresses": "Ignorar direcciones",
|
||||
@@ -250,12 +250,12 @@
|
||||
"UnableToLoadGeneralSettings": "No se han podido cargar los ajustes Generales",
|
||||
"UnableToLoadBackups": "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 ha podido añadir un nuevo indexer, 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.",
|
||||
"TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada",
|
||||
"StartTypingOrSelectAPathBelow": "Comienza a escribir o selecciona una ruta debajo",
|
||||
"Restore": "Restaurar",
|
||||
"ProwlarrSupportsAnyIndexer": "{appName} soporta cualquier indexer que utilice el estandar Newznab, como también cualquiera de los indexers listados debajo.",
|
||||
"ProwlarrSupportsAnyIndexer": "{appName} soporta muchos indexadores junto a cualquier indexador que utilice el estándar Newznab/Torznab usando 'Newznab genérico' (para usenet) o 'Torznab genérico' (para torrents). Busca y selecciona tu indexador a continuación.",
|
||||
"ProwlarrSupportsAnyDownloadClient": "{appName} soporta cualquier gestor de descargas indicado debajo.",
|
||||
"NoUpdatesAreAvailable": "No hay actualizaciones disponibles",
|
||||
"NoTagsHaveBeenAddedYet": "Ninguna etiqueta ha sido añadida aún",
|
||||
@@ -271,9 +271,9 @@
|
||||
"UILanguage": "Lenguaje de UI",
|
||||
"Priority": "Prioridad",
|
||||
"InteractiveSearch": "Búsqueda Interactiva",
|
||||
"IndexerPriorityHelpText": "Prioridad del Indexer de 1 (La más alta) a 50 (La más baja). Por defecto: 25.",
|
||||
"IndexerPriorityHelpText": "Prioridad del indexador de 1 (la más alta) a 50 (la más baja). Predeterminado: 25.",
|
||||
"IndexerPriority": "Prioridad del indexador",
|
||||
"EditIndexer": "Editar Indexer",
|
||||
"EditIndexer": "Editar indexador",
|
||||
"Disabled": "Deshabilitado",
|
||||
"AutomaticSearch": "Búsqueda Automática",
|
||||
"AddIndexer": "Añadir Indexador",
|
||||
@@ -284,8 +284,8 @@
|
||||
"MovieIndexScrollBottom": "Indice de Películas: Desplazar hacia abajo",
|
||||
"CloseCurrentModal": "Cerrar esta Ventana Modal",
|
||||
"AcceptConfirmationModal": "Aceptar Confirmación de esta Ventana Modal",
|
||||
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexers no disponible por errores durando más de 6 horas: {indexerNames}",
|
||||
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexer está disponible por errores durando más de 6 horas",
|
||||
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores durante más de 6 horas: {indexerNames}",
|
||||
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas",
|
||||
"Reddit": "Reddit",
|
||||
"UnableToAddANewAppProfilePleaseTryAgain": "No se ha podido añadir un nuevo perfil de calidad, prueba otra vez.",
|
||||
"DeleteIndexerProxyMessageText": "¿Seguro que quieres eliminar el proxy indexador '{name}'?",
|
||||
@@ -301,15 +301,15 @@
|
||||
"Today": "Hoy",
|
||||
"Tomorrow": "Mañana",
|
||||
"Torrent": "Torrents",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "No se ha podido añadir un nuevo indexer, prueba otra vez.",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "No se pudo añadir un nuevo proxy de indexador, por favor inténtalo de nuevo.",
|
||||
"Wiki": "Wiki",
|
||||
"Yesterday": "Ayer",
|
||||
"ApplicationStatusCheckAllClientMessage": "Las listas no están disponibles debido a errores",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Listas no disponibles debido a errores: {0}",
|
||||
"AllIndexersHiddenDueToFilter": "Todos los indexadores están ocultas debido al filtro aplicado.",
|
||||
"DeleteApplicationMessageText": "Seguro que quieres eliminar la notificación '{name}'?",
|
||||
"IndexerProxyStatusAllUnavailableHealthCheckMessage": "Los indexers no están disponibles debido a errores",
|
||||
"IndexerProxyStatusUnavailableHealthCheckMessage": "Indexers no disponibles debido a errores: {indexerProxyNames}",
|
||||
"IndexerProxyStatusAllUnavailableHealthCheckMessage": "Ningún indexador disponible debido a errores",
|
||||
"IndexerProxyStatusUnavailableHealthCheckMessage": "Proxies de indexador no disponibles debido a errores: {indexerProxyNames}",
|
||||
"NoLinks": "Sin enlaces",
|
||||
"AddDownloadClient": "Añadir Cliente de Descarga",
|
||||
"CouldNotConnectSignalR": "No se pudo conectar a SignalR, la interfaz de usuario no se actualiza",
|
||||
@@ -333,7 +333,7 @@
|
||||
"Applications": "Aplicaciones",
|
||||
"AppProfileInUse": "Perfil de aplicación en uso",
|
||||
"AddDownloadClientToProwlarr": "Añadir un cliente de descargas permite a {appName} enviar descargas directamente desde la interfaz en una búsqueda manual.",
|
||||
"Category": "Categoria",
|
||||
"Category": "Categoría",
|
||||
"Application": "Aplicación",
|
||||
"BookSearch": "Búsqueda de libros",
|
||||
"BookSearchTypes": "Tipos de búsqueda de libros",
|
||||
@@ -342,13 +342,13 @@
|
||||
"Encoding": "Codificación",
|
||||
"GrabReleases": "Capturar Lanzamiento(s)",
|
||||
"Link": "Enlace",
|
||||
"MappedDrivesRunningAsService": "Las unidades de red asignadas no están disponibles cuando se ejecutan como un servicio de Windows. Consulte las preguntas frecuentes para obtener más información",
|
||||
"MappedDrivesRunningAsService": "Las unidades de red asignadas no están disponibles cuando se ejecutan como un servicio de Windows. Consulta las preguntas frecuentes para obtener más información",
|
||||
"No": "No",
|
||||
"Notification": "Notificaciones",
|
||||
"Notification": "Notificación",
|
||||
"Notifications": "Notificaciones",
|
||||
"UnableToLoadIndexers": "No se pueden cargar los indexadores",
|
||||
"Yes": "Sí",
|
||||
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent proporcionado por la aplicación llamó a la API",
|
||||
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent proporcionado por la aplicación que llamó a la API",
|
||||
"InstanceName": "Nombre de la Instancia",
|
||||
"InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog",
|
||||
"Database": "Base de datos",
|
||||
@@ -374,9 +374,9 @@
|
||||
"Theme": "Tema",
|
||||
"ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente",
|
||||
"DeleteSelectedApplicationsMessageText": "¿Está seguro de querer eliminar {count} aplicación(es) de seleccionada(s)?",
|
||||
"DeleteSelectedDownloadClients": "Borrar Cliente de Descarga(s)",
|
||||
"DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?",
|
||||
"DeleteSelectedDownloadClientsMessageText": "¿Está seguro de querer eliminar {count} cliente(s) de descarga seleccionado(s)?",
|
||||
"DeleteSelectedDownloadClients": "Borrar cliente(s) de descarga",
|
||||
"DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?",
|
||||
"DeleteSelectedDownloadClientsMessageText": "¿Estás seguro que quieres eliminar {count} cliente(s) de descarga seleccionado(s)?",
|
||||
"ApplyTagsHelpTextHowToApplyApplications": "Cómo aplicar etiquetas a las aplicaciones seleccionadas",
|
||||
"SelectIndexers": "Seleccionar Indexadores",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Cómo aplicar etiquetas a los indexadores seleccionados",
|
||||
@@ -386,7 +386,7 @@
|
||||
"DownloadClientPriorityHelpText": "Priorizar varios Clientes de Descarga. Round-Robin se utiliza para clientes con la misma prioridad.",
|
||||
"Season": "Temporada",
|
||||
"More": "Más",
|
||||
"Track": "Rastro",
|
||||
"Track": "Pista",
|
||||
"Year": "Año",
|
||||
"UpdateAvailableHealthCheckMessage": "La nueva actualización está disponible",
|
||||
"Genre": "Género",
|
||||
@@ -402,17 +402,17 @@
|
||||
"ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {length} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración",
|
||||
"IndexerDownloadClientHealthCheckMessage": "Indexadores con clientes de descarga inválidos: {indexerNames}.",
|
||||
"Episode": "Episodio",
|
||||
"ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puede hacer clic en recargar abajo.",
|
||||
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.",
|
||||
"ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puedes pulsar en recargar abajo.",
|
||||
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.",
|
||||
"RecentChanges": "Cambios recientes",
|
||||
"WhatsNew": "Que es lo nuevo?",
|
||||
"minutes": "Minutos",
|
||||
"WhatsNew": "¿Qué hay nuevo?",
|
||||
"minutes": "minutos",
|
||||
"Album": "Álbum",
|
||||
"Artist": "Artista",
|
||||
"DeleteAppProfileMessageText": "¿Estás seguro de que quieres eliminar el perfil de la aplicación '{name}'?",
|
||||
"AddConnection": "Añadir Conexión",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Listas no disponibles debido a errores: {notificationNames}",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a errores",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponibles debido a errores: {notificationNames}",
|
||||
"EditIndexerImplementation": "Editar Indexador - {implementationName}",
|
||||
"AuthBasic": "Básico (ventana emergente del navegador)",
|
||||
"AuthForm": "Formularios (Página de inicio de sesión)",
|
||||
@@ -496,7 +496,7 @@
|
||||
"InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración",
|
||||
"DownloadClientQbittorrentSettingsContentLayout": "Diseño del contenido",
|
||||
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usar el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crear una subcarpeta (qBittorrent 4.3.2+)",
|
||||
"EnableRssHelpText": "Habilitar feed RSS para el Indexador",
|
||||
"EnableRssHelpText": "Habilitar canal RSS para el Indexador",
|
||||
"days": "días",
|
||||
"ElapsedTime": "Tiempo transcurrido",
|
||||
"GrabTitle": "Capturar título",
|
||||
@@ -505,17 +505,17 @@
|
||||
"Redirect": "Redirección",
|
||||
"RssQueries": "Consultas RSS",
|
||||
"SeedRatio": "Proporción de Semillado",
|
||||
"RssFeed": "Feed RSS",
|
||||
"RssFeed": "Canal RSS",
|
||||
"SearchType": "Tipo de búsqueda",
|
||||
"RepeatSearch": "Repetir búsqueda",
|
||||
"SeedRatioHelpText": "El ratio que un torrent debe alcanzar antes de detenerse, si está vacío se usará el valor por defecto",
|
||||
"SeedTime": "Tiempo de Semillado",
|
||||
"SearchTypes": "Tipos de búsquedas",
|
||||
"DeleteIndexerProxy": "Eliminar proxy indexador",
|
||||
"OnGrabHelpText": "Al Capturar lanzamiento",
|
||||
"SeedTimeHelpText": "El ratio que un torrent debe alcanzar antes de detenerse, si está vacío se usará el valor por defecto",
|
||||
"OnGrabHelpText": "Al capturar lanzamiento",
|
||||
"SeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de detenerse, si está vacío usa el valor predeterminado",
|
||||
"IndexerTagsHelpTextWarning": "Las etiquetas deben utilizarse con precaución, ya que pueden tener efectos no deseados. Un indexador con una etiqueta solo se sincronizará con aplicaciones que tengan la misma etiqueta.",
|
||||
"TVSearchTypes": "Tipos de búsquedas",
|
||||
"TVSearchTypes": "Tipos de búsqueda de TV",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación de Aria2 predeterminada",
|
||||
"IndexerNoDefCheckMessage": "Los indexadores no tienen definición y no funcionarán: {0}. Por favor elimínelos y (o) vuelva a añadirlos a {appName}",
|
||||
"IndexerProxy": "Proxy del Indexador",
|
||||
@@ -524,10 +524,10 @@
|
||||
"IndexerRss": "RSS del Indexador",
|
||||
"IndexerSettingsSummary": "Configurar varios ajustes globales del Indexador, incluyendo Proxies.",
|
||||
"IndexerName": "Nombre del Indexador",
|
||||
"IndexerVipExpiringHealthCheckMessage": "Las ventajas VIP de los indexadores expiran pronto: {indexerNames}",
|
||||
"IndexerVipExpiringHealthCheckMessage": "Las ventajas VIP del indexador expiran pronto: {indexerNames}",
|
||||
"IndexerProxies": "Proxies del Indexador",
|
||||
"IndexerHistoryLoadError": "Error al cargar el historial del Indexador",
|
||||
"IndexerVipExpiredHealthCheckMessage": "Las ventajas VIP del Indexador han caducado: {indexerNames}",
|
||||
"IndexerVipExpiredHealthCheckMessage": "Las ventajas VIP del indexador han expirado: {indexerNames}",
|
||||
"IndexerQuery": "Consulta dedl Indexador",
|
||||
"IndexerSite": "Sitio del Indexador",
|
||||
"IndexerStatus": "Estado del indexador",
|
||||
@@ -613,7 +613,7 @@
|
||||
"IndexerHDBitsSettingsPasskeyHelpText": "Clave de acceso desde los Detalles de Usuario",
|
||||
"IndexerSettingsPasskey": "Clave de Acceso",
|
||||
"BlackholeFolderHelpText": "La carpeta en donde {appName} se almacenaran los {extension} file",
|
||||
"CustomFilter": "Filtros personalizados",
|
||||
"CustomFilter": "Filtro personalizado",
|
||||
"LabelIsRequired": "Se requiere etiqueta",
|
||||
"TorrentBlackholeSaveMagnetFiles": "Guardar archivos magnet",
|
||||
"TorrentBlackholeTorrentFolder": "Carpeta de torrent",
|
||||
@@ -702,7 +702,7 @@
|
||||
"IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech",
|
||||
"IndexerBeyondHDSettingsLimitedOnlyHelpText": "Buscar solo freeleech (UL limitada)",
|
||||
"IndexerFileListSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech",
|
||||
"IndexerFileListSettingsPasskeyHelpText": "Clave de acceso del sitio (esto es la cadena alfanumérica en la url del tracket mostrada en tu cliente de descarga)",
|
||||
"IndexerFileListSettingsPasskeyHelpText": "Clave de acceso del sitio (Esto es la cadena alfanumérica en la url del tracker mostrada en tu cliente de descarga)",
|
||||
"IndexerFileListSettingsUsernameHelpText": "Usuario del sitio",
|
||||
"IndexerHDBitsSettingsMediums": "Medios",
|
||||
"IndexerHDBitsSettingsCodecs": "Códecs",
|
||||
@@ -747,5 +747,18 @@
|
||||
"IndexerSettingsCookieHelpText": "Cookie del sitio",
|
||||
"IndexerSettingsFreeleechOnly": "Solo freeleech",
|
||||
"IndexerSettingsVipExpiration": "Expiración del VIP",
|
||||
"XmlRpcPath": "Ruta RPC de XML"
|
||||
"XmlRpcPath": "Ruta RPC de XML",
|
||||
"NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título",
|
||||
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones",
|
||||
"IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Busca solo lanzamientos freeleech",
|
||||
"ProxyValidationBadRequest": "Fallo al probar el proxy. Código de estado: {statusCode}",
|
||||
"ProxyValidationUnableToConnect": "No se pudo conectar al proxy: {exceptionMessage}. Comprueba el registro de este error para más detalles",
|
||||
"GrabRelease": "Capturar lanzamiento",
|
||||
"ManualGrab": "Captura manual",
|
||||
"Open": "Abrir",
|
||||
"OverrideAndAddToDownloadClient": "Sobrescribir y añadir al cliente de descarga",
|
||||
"OverrideGrabModalTitle": "Sobrescribir y capturar - {title}",
|
||||
"PrioritySettings": "Prioridad: {priority}",
|
||||
"SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga",
|
||||
"Default": "Predeterminado"
|
||||
}
|
||||
|
||||
@@ -668,7 +668,7 @@
|
||||
"DownloadClientFreeboxSettingsPortHelpText": "Freebox-liittymän portti. Oletus on \"{port}\".",
|
||||
"DownloadClientPneumaticSettingsNzbFolder": "NZB-kansio",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrder": "Peräkkäinen järjestys",
|
||||
"CustomFilter": "Omat suodattimet",
|
||||
"CustomFilter": "Oma suodatin",
|
||||
"DownloadClientFreeboxSettingsAppIdHelpText": "Freebox-rajapinnan käyttöoikeutta määritettäessä käytettävä App ID -sovellustunniste.",
|
||||
"DownloadClientFreeboxSettingsAppToken": "Sovellustietue",
|
||||
"DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGet-version 16.0.",
|
||||
@@ -680,5 +680,6 @@
|
||||
"TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain lataustyökalun tukiessa tiedostoon tallennettuja magnet-linkkejä).",
|
||||
"TorrentBlackholeTorrentFolder": "Torrent-kansio",
|
||||
"TorrentBlackholeSaveMagnetFilesExtension": "Tallennettujen magnet-tiedostojen pääte",
|
||||
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\"."
|
||||
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\".",
|
||||
"LabelIsRequired": "Nimi on pakollinen"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Events": "Événements",
|
||||
"Edit": "Modifier",
|
||||
"DownloadClientStatusAllClientHealthCheckMessage": "Aucun client de téléchargement n'est disponible en raison d'échecs",
|
||||
"DownloadClients": "Clients de télécharg.",
|
||||
"DownloadClients": "Clients de téléchargement",
|
||||
"Dates": "Dates",
|
||||
"Date": "Date",
|
||||
"Delete": "Supprimer",
|
||||
@@ -747,5 +747,7 @@
|
||||
"ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Si un torrent est bloqué par le hachage, il peut ne pas être correctement rejeté pendant le RSS/recherche pour certains indexeurs. L'activation de cette fonction permet de le rejeter après que le torrent a été saisi, mais avant qu'il ne soit envoyé au client.",
|
||||
"ProwlarrDownloadClientsAlert": "Si vous avez l'intention d'effectuer des recherches directement dans {appName}, vous devez ajouter les clients de téléchargement. Sinon, vous n'avez pas besoin de les ajouter ici. Pour les recherches à partir de vos applications, les clients de téléchargement qui y sont configurés sont utilisés à la place.",
|
||||
"IndexerMTeamTpSettingsApiKeyHelpText": "Clé API du site (trouvée dans le panneau de configuration utilisateur => Sécurité => Laboratoire)",
|
||||
"IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Rechercher uniquement les versions freeleech"
|
||||
"IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Rechercher uniquement les versions freeleech",
|
||||
"NotificationsTelegramSettingsIncludeAppName": "Inclure {appName} dans le Titre",
|
||||
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications"
|
||||
}
|
||||
|
||||
@@ -747,5 +747,18 @@
|
||||
"ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "se um torrent for bloqueado por hash, ele pode não ser rejeitado corretamente durante o RSS/Pesquisa de alguns indexadores. Ativar isso permitirá que ele seja rejeitado após o torrent ser capturado, mas antes de ser enviado ao cliente.",
|
||||
"ClickToChangeQueryOptions": "Clique para alterar as opções de consulta",
|
||||
"IndexerMTeamTpSettingsApiKeyHelpText": "Chave API do Site (Encontrada no Painel de Controle do Usuário => Segurança => Laboratório)",
|
||||
"IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Pesquise apenas lançamentos freeleech"
|
||||
"IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Pesquise apenas lançamentos freeleech",
|
||||
"NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} no Título",
|
||||
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente, prefixe o título da mensagem com {appName} para diferenciar notificações de diferentes aplicativos",
|
||||
"IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Pesquise apenas lançamentos freeleech",
|
||||
"ProxyValidationBadRequest": "Falha ao testar o proxy. Código de status: {statusCode}",
|
||||
"ProxyValidationUnableToConnect": "Não foi possível conectar-se ao proxy: {exceptionMessage}. Verifique o log em torno deste erro para obter detalhes",
|
||||
"Default": "Padrão",
|
||||
"ManualGrab": "Baixar Manualmente",
|
||||
"Open": "Abrir",
|
||||
"OverrideAndAddToDownloadClient": "Substituir e adicionar ao cliente de download",
|
||||
"OverrideGrabModalTitle": "Substituir e Baixar - {title}",
|
||||
"PrioritySettings": "Prioridade: {priority}",
|
||||
"GrabRelease": "Baixar Lançamento",
|
||||
"SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download"
|
||||
}
|
||||
|
||||
@@ -6,21 +6,21 @@
|
||||
"Connections": "Bağlantılar",
|
||||
"Connect": "Bağlan",
|
||||
"Clear": "Temizle",
|
||||
"Sort": "Çeşitle",
|
||||
"Sort": "Sınıflandır",
|
||||
"SetTags": "Etiketleri Ayarla",
|
||||
"Scheduled": "Tarifeli",
|
||||
"Scheduled": "Planlı",
|
||||
"ProxyResolveIpHealthCheckMessage": "{proxyHostName} Yapılandırılmış Proxy Ana Bilgisayarının IP Adresi çözülemedi",
|
||||
"ProxyFailedToTestHealthCheckMessage": "Proxy ile test edilemedi: {url}",
|
||||
"ProxyBadRequestHealthCheckMessage": "Proxy ile test edilemedi. DurumKodu: {statusCode}",
|
||||
"Proxy": "Proxy",
|
||||
"Logging": "Logging",
|
||||
"Logging": "Loglama",
|
||||
"LogFiles": "Log dosyaları",
|
||||
"Host": "Ana bilgisayar",
|
||||
"GeneralSettingsSummary": "Port, SSL, kullanıcı adı/şifre, proxy, analitikler ve güncellemeler",
|
||||
"Folder": "Klasör",
|
||||
"Files": "Dosyalar",
|
||||
"Filename": "Dosya adı",
|
||||
"AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak",
|
||||
"AppDataLocationHealthCheckMessage": "Güncelleme sırasında AppData'nın silinmesini önlemek için güncelleme yapılmayacaktır",
|
||||
"Actions": "Eylemler",
|
||||
"About": "Hakkında",
|
||||
"View": "Görünüm",
|
||||
@@ -37,17 +37,17 @@
|
||||
"System": "Sistem",
|
||||
"Style": "Tarz",
|
||||
"Status": "Durum",
|
||||
"Size": "Ölçü",
|
||||
"Size": "Boyut",
|
||||
"ShowAdvanced": "Gelişmiş'i Göster",
|
||||
"Settings": "Ayarlar",
|
||||
"SelectAll": "Hepsini seç",
|
||||
"SelectAll": "Hepsini Seç",
|
||||
"Security": "Güvenlik",
|
||||
"Search": "Ara",
|
||||
"SaveChanges": "Değişiklikleri Kaydet",
|
||||
"ReleaseStatus": "Yayın Durumu",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "Dal {0} geçerli bir {appName} sürüm dalı değil, güncelleme almayacaksınız",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "{0} şubesi geçerli bir {appName} sürüm dalı değil; güncelleme almayacaksınız",
|
||||
"Refresh": "Yenile",
|
||||
"Queue": "Sıra",
|
||||
"Queue": "Sırada",
|
||||
"Protocol": "Protokol",
|
||||
"Options": "Seçenekler",
|
||||
"NoChanges": "Değişiklikler yok",
|
||||
@@ -55,7 +55,7 @@
|
||||
"MoreInfo": "Daha fazla bilgi",
|
||||
"LastWriteTime": "Son Yazma Zamanı",
|
||||
"Language": "Dil",
|
||||
"History": "Tarih",
|
||||
"History": "Geçmiş",
|
||||
"HideAdvanced": "Gelişmiş'i Gizle",
|
||||
"Health": "Sağlık",
|
||||
"General": "Genel",
|
||||
@@ -65,7 +65,7 @@
|
||||
"CustomFilters": "Özel Filtreler",
|
||||
"ConnectSettingsSummary": "Bildirimler, medya sunucularına/oynatıcılara bağlantılar ve özel komut kodları",
|
||||
"Analytics": "Analitik",
|
||||
"All": "Herşey",
|
||||
"All": "Hepsi",
|
||||
"Added": "Eklendi",
|
||||
"Add": "Ekle",
|
||||
"Branch": "Şube",
|
||||
@@ -83,9 +83,9 @@
|
||||
"UnableToLoadTags": "Etiketler yüklenemiyor",
|
||||
"UnsavedChanges": "Kaydedilmemiş Değişiklikler",
|
||||
"Backups": "Yedeklemeler",
|
||||
"BindAddress": "Bağlama Adresi",
|
||||
"BindAddress": "Bind Adresi",
|
||||
"BypassProxyForLocalAddresses": "Yerel Adresler için Proxy'yi Atla",
|
||||
"DeleteNotificationMessageText": "'{0}' bildirimini silmek istediğinizden emin misiniz?",
|
||||
"DeleteNotificationMessageText": "'{name}' bildirimini silmek istediğinizden emin misiniz?",
|
||||
"EnableSslHelpText": " Etkili olması için yönetici olarak yeniden çalıştırmayı gerektirir",
|
||||
"Fixed": "Sabit",
|
||||
"PendingChangesMessage": "Kaydedilmemiş değişiklikleriniz var, bu sayfadan ayrılmak istediğinizden emin misiniz?",
|
||||
@@ -94,14 +94,14 @@
|
||||
"PortNumber": "Port numarası",
|
||||
"RestoreBackup": "Yedeği Geri Yükle",
|
||||
"Rss": "RSS",
|
||||
"Save": "Kayıt etmek",
|
||||
"Save": "Kaydet",
|
||||
"SaveSettings": "Ayarları kaydet",
|
||||
"ScriptPath": "Komut Dosyası Yolu",
|
||||
"Test": "Ölçek",
|
||||
"Test": "Sına",
|
||||
"TestAll": "Tümünü Test Et",
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.",
|
||||
"YesCancel": "Evet İptal",
|
||||
"ApplicationStatusCheckAllClientMessage": "Hatalar nedeniyle tüm listeler kullanılamıyor",
|
||||
"ApplicationStatusCheckAllClientMessage": "Hatalar nedeniyle tüm uygulamalar kullanılamıyor",
|
||||
"CancelPendingTask": "Bu bekleyen görevi iptal etmek istediğinizden emin misiniz?",
|
||||
"DeleteTag": "Etiketi Sil",
|
||||
"BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'",
|
||||
@@ -109,9 +109,9 @@
|
||||
"DatabaseMigration": "DB Geçişi",
|
||||
"DeleteApplicationMessageText": "'{0}' bildirimini silmek istediğinizden emin misiniz?",
|
||||
"DeleteBackup": "Yedeklemeyi Sil",
|
||||
"DeleteBackupMessageText": "'{0}' yedeğini silmek istediğinizden emin misiniz?",
|
||||
"DeleteBackupMessageText": "'{name}' yedeğini silmek istediğinizden emin misiniz?",
|
||||
"DeleteDownloadClient": "İndirme İstemcisini Sil",
|
||||
"DeleteDownloadClientMessageText": "İndirme istemcisini '{0}' silmek istediğinizden emin misiniz?",
|
||||
"DeleteDownloadClientMessageText": "'{name}' indirme istemcisini silmek istediğinizden emin misiniz?",
|
||||
"DownloadClientSettings": "İstemci Ayarlarını İndir",
|
||||
"EnableAutomaticSearchHelpText": "Kullanıcı arayüzü veya {appName} tarafından otomatik aramalar yapıldığında kullanılacaktır",
|
||||
"ForMoreInformationOnTheIndividualDownloadClients": "Bireysel indirme istemcileri hakkında daha fazla bilgi için bilgi düğmelerine tıklayın.",
|
||||
@@ -124,7 +124,7 @@
|
||||
"MovieIndexScrollBottom": "Film Dizini: Alta Kaydırma",
|
||||
"MovieIndexScrollTop": "Film Dizini: Yukarı Kaydırma",
|
||||
"NoLinks": "Bağlantı Yok",
|
||||
"PackageVersion": "Paket Sürümü",
|
||||
"PackageVersion": "Paket Versiyonu",
|
||||
"PageSize": "Sayfa boyutu",
|
||||
"PageSizeHelpText": "Her sayfada gösterilecek öğe sayısı",
|
||||
"ProxyBypassFilterHelpText": "Ayırıcı olarak \",\" ve \"*\" kullanın. alt alan adları için joker karakter olarak",
|
||||
@@ -132,9 +132,9 @@
|
||||
"Reload": "Tekrar yükle",
|
||||
"RemovedFromTaskQueue": "Görev kuyruğundan kaldırıldı",
|
||||
"SendAnonymousUsageData": "Anonim Kullanım Verilerini Gönderin",
|
||||
"Age": "Yaş",
|
||||
"AllIndexersHiddenDueToFilter": "Uygulanan filtre nedeniyle tüm filmler gizlendi.",
|
||||
"AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Bu, tarayıcınızla ilgili bilgileri, kullandığınız {appName} WebUI sayfalarını, hata raporlamasının yanı sıra işletim sistemi ve çalışma zamanı sürümünü içerir. Bu bilgileri, özellikleri ve hata düzeltmelerini önceliklendirmek için kullanacağız.",
|
||||
"Age": "Yıl",
|
||||
"AllIndexersHiddenDueToFilter": "Uygulanan filtre nedeniyle tüm dizin oluşturucular gizlendi.",
|
||||
"AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Buna, tarayıcınız, hangi {appName} WebUI sayfalarını kullandığınız, hata raporlamanın yanı sıra işletim sistemi ve çalışma zamanı sürümü hakkındaki bilgiler de dahildir. Bu bilgiyi özelliklere ve hata düzeltmelerine öncelik vermek için kullanacağız.",
|
||||
"ApiKey": "API Anahtarı",
|
||||
"AppDataDirectory": "Uygulama Veri Dizini",
|
||||
"NoUpdatesAreAvailable": "Güncelleme yok",
|
||||
@@ -143,14 +143,14 @@
|
||||
"OnHealthIssueHelpText": "Sağlık Sorunu Hakkında",
|
||||
"BranchUpdate": "{appName}'ı güncellemek için kullanılacak dal",
|
||||
"Close": "Kapat",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Hatalar nedeniyle kullanılamayan listeler: {0}",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Hatalar nedeniyle kullanılamayan uygulamalar: {0}",
|
||||
"ApplyTags": "Etiketleri Uygula",
|
||||
"RssIsNotSupportedWithThisIndexer": "RSS, bu indeksleyici ile desteklenmiyor",
|
||||
"Interval": "Aralık",
|
||||
"Interval": "Periyot",
|
||||
"Logs": "Kütükler",
|
||||
"Authentication": "Doğrulama",
|
||||
"AuthenticationMethodHelpText": "{appName}'a erişmek için Kullanıcı Adı ve Şifre gerektir",
|
||||
"BackupIntervalHelpText": "Otomatik yedeklemeler arasındaki aralık",
|
||||
"BackupIntervalHelpText": "Otomatik yedeklemeler arasındaki zaman aralığı",
|
||||
"BackupNow": "Şimdi yedekle",
|
||||
"BackupRetentionHelpText": "Saklama süresinden daha eski olan otomatik yedeklemeler otomatik olarak temizlenecektir",
|
||||
"BeforeUpdate": "Güncellemeden önce",
|
||||
@@ -166,13 +166,13 @@
|
||||
"ConnectionLost": "Bağlantı koptu",
|
||||
"DeleteIndexerProxyMessageText": "'{0}' etiketini silmek istediğinizden emin misiniz?",
|
||||
"DeleteNotification": "Bildirimi Sil",
|
||||
"DeleteTagMessageText": "'{0}' etiketini silmek istediğinizden emin misiniz?",
|
||||
"DeleteTagMessageText": "'{label}' etiketini silmek istediğinizden emin misiniz?",
|
||||
"Disabled": "Devre dışı",
|
||||
"Discord": "Uyuşmazlık",
|
||||
"Docker": "Liman işçisi",
|
||||
"Donations": "Bağışlar",
|
||||
"DownloadClient": "İstemciyi İndir",
|
||||
"DownloadClients": "İstemcileri İndir",
|
||||
"DownloadClients": "İndirme İstemcileri",
|
||||
"EnableAutomaticSearch": "Otomatik Aramayı Etkinleştir",
|
||||
"EnableInteractiveSearchHelpText": "Etkileşimli arama kullanıldığında kullanılacak",
|
||||
"FocusSearchBox": "Arama Kutusuna Odaklan",
|
||||
@@ -182,10 +182,10 @@
|
||||
"HomePage": "Ana Sayfa",
|
||||
"IllRestartLater": "Daha sonra yeniden başlayacağım",
|
||||
"IncludeHealthWarningsHelpText": "Sağlık Uyarılarını Dahil Et",
|
||||
"IndexerFlags": "Dizin Oluşturucu Bayrakları",
|
||||
"IndexerFlags": "Dizinleyici Bayrakları",
|
||||
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6 saatten uzun süren arızalar nedeniyle tüm dizinleyiciler kullanılamıyor",
|
||||
"IndexerLongTermStatusUnavailableHealthCheckMessage": "6 saatten uzun süredir yaşanan arızalar nedeniyle dizinleyiciler kullanılamıyor: {indexerNames}",
|
||||
"IndexerPriority": "Dizin Oluşturucu Önceliği",
|
||||
"IndexerPriority": "Dizinleyici Önceliği",
|
||||
"IndexerPriorityHelpText": "1 (En Yüksek) ila 50 (En Düşük) arasında Dizin Oluşturucu Önceliği. Varsayılan: 25.",
|
||||
"IndexerStatusAllUnavailableHealthCheckMessage": "Hatalar nedeniyle tüm dizinleyiciler kullanılamıyor",
|
||||
"IndexerStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle dizinleyiciler kullanılamıyor: {indexerNames}",
|
||||
@@ -224,7 +224,7 @@
|
||||
"SSLCertPathHelpText": "Pfx dosyasının yolu",
|
||||
"SSLPort": "SSL Bağlantı Noktası",
|
||||
"StartTypingOrSelectAPathBelow": "Yazmaya başlayın veya aşağıdan bir yol seçin",
|
||||
"StartupDirectory": "Başlangıç dizini",
|
||||
"StartupDirectory": "Başlangıç Dizini",
|
||||
"SuggestTranslationChange": "Çeviri değişikliği önerin",
|
||||
"SystemTimeCheckMessage": "Sistem saati 1 günden fazla kapalı. Zamanlanan görevler, saat düzeltilene kadar doğru çalışmayabilir",
|
||||
"TableOptionsColumnsMessage": "Hangi sütunların görünür olduğunu ve hangi sırada görüneceklerini seçin",
|
||||
@@ -247,8 +247,8 @@
|
||||
"UnableToLoadNotifications": "Bildirimler yüklenemiyor",
|
||||
"UnableToLoadUISettings": "UI ayarları yüklenemiyor",
|
||||
"Yesterday": "Dün",
|
||||
"AcceptConfirmationModal": "Onay Modelini Kabul Et",
|
||||
"AddIndexer": "Dizin Oluşturucu Ekle",
|
||||
"AcceptConfirmationModal": "Onay Modunu Kabul Et",
|
||||
"AddIndexer": "Dizinleyici Ekle",
|
||||
"AddDownloadClient": "İndirme İstemcisi Ekle",
|
||||
"AddingTag": "Etiket ekleniyor",
|
||||
"CouldNotConnectSignalR": "SignalR'ye bağlanılamadı, kullanıcı arayüzü güncellenmeyecek",
|
||||
@@ -256,7 +256,7 @@
|
||||
"DownloadClientStatusSingleClientHealthCheckMessage": "Hatalar nedeniyle indirilemeyen istemciler: {downloadClientNames}",
|
||||
"Enabled": "Etkin",
|
||||
"IgnoredAddresses": "Yoksayılan Adresler",
|
||||
"Indexer": "Dizin oluşturucu",
|
||||
"Indexer": "Dizinleyici",
|
||||
"DownloadClientStatusAllClientHealthCheckMessage": "Hatalar nedeniyle tüm indirme istemcileri kullanılamıyor",
|
||||
"EditIndexer": "Dizinleyiciyi Düzenle",
|
||||
"Enable": "etkinleştirme",
|
||||
@@ -271,7 +271,7 @@
|
||||
"ExistingTag": "Mevcut etiket",
|
||||
"IndexerProxyStatusAllUnavailableHealthCheckMessage": "Hatalar nedeniyle tüm dizinleyiciler kullanılamıyor",
|
||||
"IndexerProxyStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle dizinleyiciler kullanılamıyor: {indexerProxyNames}",
|
||||
"Indexers": "Dizin oluşturucular",
|
||||
"Indexers": "Dizinleyiciler",
|
||||
"Name": "İsim",
|
||||
"New": "Yeni",
|
||||
"NoBackupsAreAvailable": "Kullanılabilir yedek yok",
|
||||
@@ -285,7 +285,7 @@
|
||||
"TagsHelpText": "En az bir eşleşen etikete sahip filmler için geçerlidir",
|
||||
"UILanguage": "UI Dili",
|
||||
"UpdateScriptPathHelpText": "Çıkarılan bir güncelleme paketini alan ve güncelleme işleminin geri kalanını işleyen özel bir komut dosyasına giden yol",
|
||||
"Uptime": "Uptime",
|
||||
"Uptime": "Çalışma süresi",
|
||||
"URLBase": "URL Tabanı",
|
||||
"UrlBaseHelpText": "Ters proxy desteği için varsayılan boştur",
|
||||
"Usenet": "Usenet",
|
||||
@@ -294,7 +294,7 @@
|
||||
"Version": "Sürüm",
|
||||
"Warn": "Uyar",
|
||||
"Wiki": "Wiki",
|
||||
"Apply": "Uygulamak",
|
||||
"Apply": "Uygula",
|
||||
"BackupFolderHelpText": "Göreli yollar {appName}'ın AppData dizini altında olacaktır",
|
||||
"Grabbed": "Yakalandı",
|
||||
"ProxyPasswordHelpText": "Gerekirse yalnızca bir kullanıcı adı ve şifre girmeniz gerekir. Aksi takdirde boş bırakın.",
|
||||
@@ -305,32 +305,32 @@
|
||||
"UnableToLoadGeneralSettings": "Genel ayarlar yüklenemiyor",
|
||||
"Automatic": "Otomatik",
|
||||
"AutomaticSearch": "Otomatik Arama",
|
||||
"Backup": "Destek olmak",
|
||||
"Backup": "Yedekler",
|
||||
"Cancel": "Vazgeç",
|
||||
"Level": "Seviye",
|
||||
"Time": "Zaman",
|
||||
"MaintenanceRelease": "Bakım Sürümü: hata düzeltmeleri ve diğer iyileştirmeler. Daha fazla ayrıntı için Github İşlem Geçmişine bakın",
|
||||
"HistoryCleanupDaysHelpText": "Otomatik temizlemeyi devre dışı bırakmak için 0'a ayarlayın",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Geri dönüşüm kutusundaki, seçilen gün sayısından daha eski olan dosyalar otomatik olarak temizlenecektir",
|
||||
"Filters": "Filtre",
|
||||
"Filters": "Filtreler",
|
||||
"OnGrab": "Yakalandığında",
|
||||
"OnHealthIssue": "Sağlık Sorunu Hakkında",
|
||||
"TestAllIndexers": "Tüm Dizinleyicileri Test Et",
|
||||
"GrabReleases": "Bırakma",
|
||||
"No": "Hayır",
|
||||
"NetCore": ".NET Çekirdeği",
|
||||
"NetCore": ".NET",
|
||||
"UnableToLoadIndexers": "Dizinleyiciler yüklenemiyor",
|
||||
"Yes": "Evet",
|
||||
"Link": "Bağlantılar",
|
||||
"MappedDrivesRunningAsService": "Eşlenen ağ sürücüleri, bir Windows Hizmeti olarak çalışırken kullanılamaz. Daha fazla bilgi için lütfen SSS bölümüne bakın",
|
||||
"Ended": "Bitti",
|
||||
"LastDuration": "lastDuration",
|
||||
"LastDuration": "Yürütme Süresi",
|
||||
"LastExecution": "Son Yürütme",
|
||||
"NextExecution": "Sonraki Yürütme",
|
||||
"Queued": "Sıraya alındı",
|
||||
"Queued": "Kuyruğa alındı",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "6 saatten uzun süren arızalar nedeniyle tüm dizinleyiciler kullanılamıyor",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "6 saatten uzun süredir yaşanan arızalar nedeniyle dizinleyiciler kullanılamıyor: {0}",
|
||||
"Remove": "Kaldırmak",
|
||||
"Remove": "Kaldır",
|
||||
"Replace": "Değiştir",
|
||||
"TheLatestVersionIsAlreadyInstalled": "{appName}'ın en son sürümü zaten kurulu",
|
||||
"ApplyTagsHelpTextAdd": "Ekle: Etiketleri mevcut etiket listesine ekleyin",
|
||||
@@ -338,7 +338,7 @@
|
||||
"ApplyTagsHelpTextRemove": "Kaldır: Girilen etiketleri kaldırın",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Seçilen indeksleyicilere etiketler nasıl uygulanır?",
|
||||
"ApplyTagsHelpTextReplace": "Değiştir: Etiketleri girilen etiketlerle değiştirin (tüm etiketleri kaldırmak için etiket girmeyin)",
|
||||
"DeleteSelectedDownloadClients": "İndirme İstemcisini Sil",
|
||||
"DeleteSelectedDownloadClients": "İndirme İstemcilerini Sil",
|
||||
"DownloadClientPriorityHelpText": "Birden çok İndirme İstemcisine öncelik verin. Round-Robin, aynı önceliğe sahip müşteriler için kullanılır.",
|
||||
"Genre": "Türler",
|
||||
"Track": "İzleme",
|
||||
@@ -349,8 +349,8 @@
|
||||
"minutes": "Dakika",
|
||||
"WhatsNew": "Ne var ne yok?",
|
||||
"ConnectionLostReconnect": "{appName} otomatik bağlanmayı deneyecek veya aşağıda yeniden yükle seçeneğini işaretleyebilirsiniz.",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "Hatalar nedeniyle tüm listeler kullanılamıyor",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Hatalar nedeniyle kullanılamayan listeler: {notificationNames}",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Arızalar nedeniyle bildirimler kullanılamıyor: {notificationNames}",
|
||||
"Applications": "Uygulamalar",
|
||||
"AuthBasic": "Temel (Tarayıcı Açılır Penceresi)",
|
||||
"AuthForm": "Formlar (Giriş Sayfası)",
|
||||
@@ -358,7 +358,7 @@
|
||||
"None": "Yok",
|
||||
"ResetAPIKeyMessageText": "API Anahtarınızı sıfırlamak istediğinizden emin misiniz?",
|
||||
"Categories": "Kategoriler",
|
||||
"Application": "Uygulamalar",
|
||||
"Application": "Uygulama",
|
||||
"Episode": "bölüm",
|
||||
"AddConnection": "Bağlantı Ekle",
|
||||
"AddApplicationImplementation": "Uygulama Ekle - {implementationName}",
|
||||
@@ -372,12 +372,12 @@
|
||||
"EditIndexerProxyImplementation": "Koşul Ekle - {implementationName}",
|
||||
"AddCustomFilter": "Özel Filtre Ekleyin",
|
||||
"AddDownloadClientImplementation": "İndirme İstemcisi Ekle - {implementationName}",
|
||||
"EditDownloadClientImplementation": "İndirme İstemcisi Ekle - {implementationName}",
|
||||
"EditDownloadClientImplementation": "İndirme İstemcisini Düzenle - {implementationName}",
|
||||
"ApplicationURL": "Uygulama URL'si",
|
||||
"AuthenticationRequired": "Kimlik Doğrulama Gerekli",
|
||||
"ApplyChanges": "Değişiklikleri Uygula",
|
||||
"CountDownloadClientsSelected": "{count} indirme istemcisi seçildi",
|
||||
"CountIndexersSelected": "{count} dizin oluşturucu seçildi",
|
||||
"CountIndexersSelected": "{count} dizinleyici seçildi",
|
||||
"AuthenticationRequiredWarning": "Kimlik doğrulaması olmadan uzaktan erişimi engellemek için, {appName}'da artık kimlik doğrulamanın etkinleştirilmesini gerektiriyor. İsteğe bağlı olarak yerel adresler için kimlik doğrulamayı devre dışı bırakabilirsiniz.",
|
||||
"Clone": "Klon",
|
||||
"Category": "Kategori",
|
||||
@@ -398,7 +398,102 @@
|
||||
"AddDownloadClientToProwlarr": "İndirme istemcisi eklemek, görsel arayüz üzerinde manuel arama yaparak indirilecek içeriği {appName} uygulamasına direkt olarak eklemenize olanak sağlar.",
|
||||
"AddApplication": "Uygulama Ekle",
|
||||
"AddCategory": "Kategori Ekle",
|
||||
"AddNewIndexer": "Yeni İçerik Sağlayıcı Ekle",
|
||||
"AddNewIndexer": "Yeni Dizin Oluşturucu Ekle",
|
||||
"ActiveApps": "Aktif Uygulamalar",
|
||||
"ActiveIndexers": "Aktif İçerik Kaynakları"
|
||||
"ActiveIndexers": "Aktif Dizin Oluşturucular",
|
||||
"AdvancedSettingsHiddenClickToShow": "Gelimiş ayarlar gizli, göstermek için tıklayın",
|
||||
"AddIndexerProxy": "Dizin Oluşturucu Vekili Ekle",
|
||||
"AddedToDownloadClient": "İçerik istemciye eklendi",
|
||||
"Album": "Albüm",
|
||||
"AdvancedSettingsShownClickToHide": "Gelişmiş ayarlar gösteriliyor, gizlemek için tıklayın",
|
||||
"AddToDownloadClient": "İçeriği indirme istemcisine ekle",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan Aria2 konumunu kullanmak için boş bırakın",
|
||||
"Donate": "Bağış yap",
|
||||
"Destination": "Hedef",
|
||||
"Directory": "Rehber",
|
||||
"DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın",
|
||||
"DownloadClientFloodSettingsAdditionalTags": "Ek Etiketler",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Deluge json URL'sine bir önek ekler, bkz. {url}",
|
||||
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Medyanın özelliklerini etiket olarak ekler. İpuçları örnektir.",
|
||||
"DownloadClientFloodSettingsTagsHelpText": "Bir indirme işleminin başlangıç etiketleri. Bir indirmenin tanınabilmesi için tüm başlangıç etiketlerine sahip olması gerekir. Bu, ilgisiz indirmelerle çakışmaları önler.",
|
||||
"DownloadClientFloodSettingsUrlBaseHelpText": "Flood API'sine {url} gibi bir önek ekler",
|
||||
"Database": "Veri tabanı",
|
||||
"DefaultNameCopiedProfile": "{name} - Kopyala",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?",
|
||||
"DeleteSelectedIndexersMessageText": "Seçilen {count} dizinleyiciyi silmek istediğinizden emin misiniz?",
|
||||
"DownloadClientFreeboxSettingsPortHelpText": "Freebox arayüzüne erişim için kullanılan bağlantı noktası, varsayılan olarak '{port}' şeklindedir",
|
||||
"ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter sayısı kadar güncelleyiniz. Bunu ayarlar veya yapılandırma dosyası üzerinden yapabilirsiniz",
|
||||
"AppProfileInUse": "Kullanımda Olan Uygulama Profili",
|
||||
"ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Eğer bir torrent hash değeri bazlı engellendi ise bazı dizin oluşturucular RSS/Arama sırasında bu torrenti gerektiği gibi reddedemeyebilir, bunu aktif etmek torrentin çekildikten sonra reddedilebilmesine izin verecektir, ama istemciye gönderilmeden önce.",
|
||||
"AppProfileSelectHelpText": "Uygulama profilleri, Uygulama eşitlemede RSS, Otomatik Arama ve İnteraktif Arama ayarlarını kontrol etmek için kullanılır",
|
||||
"ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "İçerik eklenirken eşitleme ret işlemi, Torrent hash değerlerini kara listeye aldı",
|
||||
"AppSettingsSummary": "{appName} uygulamasının PVR progranlarına müdahele etmek için gerekli konfigürasyonda kullanılan uygulama ve ayarlar",
|
||||
"DownloadClientFreeboxSettingsApiUrl": "API URL'si",
|
||||
"DownloadClientFreeboxSettingsApiUrlHelpText": "Freebox API temel URL'sini API sürümüyle tanımlayın, örneğin '{url}', varsayılan olarak '{defaultApiUrl}' olur",
|
||||
"DownloadClientFreeboxSettingsAppIdHelpText": "Freebox API'sine erişim oluşturulurken verilen uygulama kimliği (ör. 'app_id')",
|
||||
"DownloadClientFreeboxSettingsAppToken": "Uygulama Jetonu",
|
||||
"DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama jetonu (ör. 'app_token')",
|
||||
"DownloadClientFreeboxSettingsAppId": "Uygulama kimliği",
|
||||
"DownloadClientFreeboxSettingsHostHelpText": "Freebox'un ana bilgisayar adı veya ana bilgisayar IP adresi, varsayılan olarak '{url}' şeklindedir (yalnızca aynı ağdaysa çalışır)",
|
||||
"Duration": "Süre",
|
||||
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent'in yapılandırılmış içerik düzenini mi, torrentteki orijinal düzeni mi kullanacağınızı yoksa her zaman bir alt klasör oluşturup oluşturmayacağınızı (qBittorrent 4.3.2+)",
|
||||
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Torrentlerin başlangıç durumu qBittorrent'e eklendi. Zorunlu Torrentlerin seed kısıtlamalarına uymadığını unutmayın",
|
||||
"DownloadClientRTorrentSettingsUrlPath": "URL Yolu",
|
||||
"DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC uç noktasının yolu, bkz. {url}. RuTorrent kullanılırken bu genellikle RPC2 veya [ruTorrent yolu]{url2} olur.",
|
||||
"DownloadClientSettingsInitialState": "Başlangıç Durumu",
|
||||
"DownloadClientSettingsUrlBaseHelpText": "{clientName} URL'sine {url} gibi bir önek ekler",
|
||||
"DownloadClientSettingsInitialStateHelpText": "{clientName} dosyasına eklenen torrentler için başlangıç durumu",
|
||||
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son İlk",
|
||||
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Etkinleştirme, durdurulmuş durumdaki rTorrent'e torrentler ve magnet ekleyecektir. Bu magnet dosyalarını bozabilir.",
|
||||
"DownloadClientSettingsAddPaused": "Ekleme Durduruldu",
|
||||
"DownloadClientSettingsDestinationHelpText": "İndirme hedefini manuel olarak belirtir, varsayılanı kullanmak için boş bırakın",
|
||||
"DownloadClientSettingsUseSslHelpText": "{clientName} ile bağlantı kurulurken güvenli bağlantıyı kullan",
|
||||
"DownloadClientTransmissionSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan İletim konumunu kullanmak için boş bırakın",
|
||||
"DownloadClientPneumaticSettingsStrmFolder": "Strm Klasörü",
|
||||
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Bu klasördeki .strm dosyaları drone ile içe aktarılacak",
|
||||
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Önce ilk ve son parçaları indirin (qBittorrent 4.1.0+)",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrder": "Sıralı Sıra",
|
||||
"DownloadClientRTorrentSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan rTorrent konumunu kullanmak için boş bırakın",
|
||||
"DownloadClientTransmissionSettingsUrlBaseHelpText": "{clientName} rpc URL'sine bir önek ekler, örneğin {url}, varsayılan olarak '{defaultUrl}' olur",
|
||||
"DownloadClientQbittorrentSettingsContentLayout": "İçerik Düzeni",
|
||||
"DownloadClientPneumaticSettingsNzbFolder": "Nzb Klasörü",
|
||||
"DownloadClientNzbgetSettingsAddPausedHelpText": "Bu seçenek en az NzbGet sürüm 16.0'ı gerektirir",
|
||||
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Bu klasöre XBMC'den erişilmesi gerekecek",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Sıralı olarak indirin (qBittorrent 4.1.0+)",
|
||||
"DownloadClientQbittorrentSettingsUseSslHelpText": "Güvenli bir bağlantı kullanın. qBittorrent'te Seçenekler -> Web Kullanıcı Arayüzü -> 'HTTP yerine HTTPS kullan' bölümüne bakın.",
|
||||
"DownloadClientRTorrentSettingsAddStopped": "Ekleme Durduruldu",
|
||||
"Label": "Etiket",
|
||||
"EditSelectedDownloadClients": "Seçilen İndirme İstemcilerini Düzenle",
|
||||
"EditSelectedIndexers": "Seçili Dizinleyicileri Düzenle",
|
||||
"NoIndexersFound": "Dizinleyici bulunamadı",
|
||||
"NoHistoryFound": "Geçmiş bulunamadı",
|
||||
"ManageDownloadClients": "İndirme İstemcilerini Yönet",
|
||||
"InstanceNameHelpText": "Sekmedeki örnek adı ve Syslog uygulaması adı için",
|
||||
"LabelIsRequired": "Etiket gerekli",
|
||||
"ManageClients": "İstemcileri Yönet",
|
||||
"Implementation": "Uygula",
|
||||
"NotificationsEmailSettingsUseEncryptionHelpText": "Sunucuda yapılandırılmışsa şifrelemeyi kullanmayı mı tercih edeceğiniz, şifrelemeyi her zaman SSL (yalnızca Bağlantı Noktası 465) veya StartTLS (başka herhangi bir bağlantı noktası) aracılığıyla mı kullanacağınız veya hiçbir zaman şifrelemeyi kullanmama tercihlerini belirler",
|
||||
"Menu": "Menü",
|
||||
"NotificationsEmailSettingsUseEncryption": "Şifreleme Kullan",
|
||||
"InvalidUILanguage": "Kullanıcı arayüzünüz geçersiz bir dile ayarlanmış, düzeltin ve ayarlarınızı kaydedin",
|
||||
"InstanceName": "Örnek isim",
|
||||
"NoDownloadClientsFound": "İndirme istemcisi bulunamadı",
|
||||
"NotificationTriggersHelpText": "Bu bildirimi hangi olayların tetikleyeceğini seçin",
|
||||
"Theme": "Tema",
|
||||
"OnHealthRestored": "Sağlığın İyileştirilmesi Hakkında",
|
||||
"NotificationsTelegramSettingsIncludeAppName": "{appName}'i Başlığa dahil et",
|
||||
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Farklı uygulamalardan gelen bildirimleri ayırt etmek için isteğe bağlı olarak mesaj başlığının önüne {appName} ekleyin",
|
||||
"UseSsl": "SSL kullan",
|
||||
"OnApplicationUpdate": "Uygulama Güncellemesinde",
|
||||
"TorrentBlackholeSaveMagnetFiles": "Magnet Dosyalarını Kaydet",
|
||||
"SecretToken": "Gizlilik Jetonu",
|
||||
"TorrentBlackholeSaveMagnetFilesExtension": "Magnet Dosya Uzantısını Kaydet",
|
||||
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet bağlantıları için kullanılacak uzantı, varsayılan olarak '.magnet'tir",
|
||||
"TorrentBlackholeSaveMagnetFilesHelpText": ".torrent dosyası yoksa magnet bağlantısını kaydedin (yalnızca indirme istemcisi bir dosyaya kaydedilen magnetleri destekliyorsa kullanışlıdır)",
|
||||
"TorrentBlackholeTorrentFolder": "Torrent Klasörü",
|
||||
"PasswordConfirmation": "Şifre onayı",
|
||||
"StopSelecting": "Düzenlemeden Çık",
|
||||
"Started": "Başlatıldı",
|
||||
"UsenetBlackholeNzbFolder": "Nzb Klasörü",
|
||||
"XmlRpcPath": "XML RPC Yolu"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"ApiKey": "API Ключ",
|
||||
"Added": "Додано",
|
||||
"AddIndexer": "Додати Індексер",
|
||||
"AddingTag": "Додавання тегу",
|
||||
"AddingTag": "Додавання тега",
|
||||
"AppDataDirectory": "Каталог AppData",
|
||||
"AppDataLocationHealthCheckMessage": "Оновлення буде неможливим, щоб запобігти видаленню AppData під час оновлення",
|
||||
"Apply": "Застосувати",
|
||||
@@ -29,19 +29,19 @@
|
||||
"Database": "База даних",
|
||||
"Date": "Дата",
|
||||
"Dates": "Дати",
|
||||
"DatabaseMigration": "Міграція БД",
|
||||
"DatabaseMigration": "Міграція бази даних",
|
||||
"DeleteBackup": "Видалити резервну копію",
|
||||
"DeleteBackupMessageText": "Ви впевнені, що хочете видалити резервну копію '{0}'?",
|
||||
"DeleteBackupMessageText": "Ви впевнені, що хочете видалити резервну копію \"{name}\"?",
|
||||
"DeleteDownloadClient": "Видалити клієнт завантаження",
|
||||
"DeleteDownloadClientMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?",
|
||||
"DeleteDownloadClientMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{name}'?",
|
||||
"Authentication": "Автентифікація",
|
||||
"Automatic": "Автоматичний",
|
||||
"Automatic": "Автоматично",
|
||||
"AutomaticSearch": "Автоматичний пошук",
|
||||
"Backup": "Резервне копіювання",
|
||||
"BackupIntervalHelpText": "Інтервал між автоматичним резервним копіюванням",
|
||||
"Backups": "Резервні копії",
|
||||
"BeforeUpdate": "Перед оновленням",
|
||||
"BindAddress": "Прив'язувати адресу",
|
||||
"BindAddress": "Прив'язати адресу",
|
||||
"Branch": "Гілка",
|
||||
"BypassProxyForLocalAddresses": "Обійти проксі для локальних адрес",
|
||||
"Cancel": "Скасувати",
|
||||
@@ -53,7 +53,7 @@
|
||||
"Close": "Закрити",
|
||||
"CloseCurrentModal": "Закрити поточне вікно",
|
||||
"Columns": "Колонки",
|
||||
"AuthenticationMethodHelpText": "Для доступу до {appName} потрібні ім’я користувача та пароль",
|
||||
"AuthenticationMethodHelpText": "Вимагати ім’я користувача та пароль для доступу до {appName}",
|
||||
"BackupFolderHelpText": "Відносні шляхи будуть у каталозі AppData {appName}",
|
||||
"BindAddressHelpText": "Дійсна адреса IP або '*' для всіх інтерфейсів",
|
||||
"BranchUpdate": "Гілка для оновлення {appName}",
|
||||
@@ -61,9 +61,9 @@
|
||||
"AnalyticsEnabledHelpText": "Надсилайте анонімну інформацію про використання та помилки на сервери {appName}. Це включає інформацію про ваш веб-переглядач, які сторінки {appName} WebUI ви використовуєте, звіти про помилки, а також версію ОС і часу виконання. Ми будемо використовувати цю інформацію, щоб визначити пріоритети функцій і виправлення помилок.",
|
||||
"Delete": "Видалити",
|
||||
"DeleteApplicationMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?",
|
||||
"DeleteTagMessageText": "Ви впевнені, що хочете видалити тег {0} ?",
|
||||
"DeleteTagMessageText": "Ви впевнені, що хочете видалити тег '{label}'?",
|
||||
"DeleteIndexerProxyMessageText": "Ви впевнені, що хочете видалити тег {0} ?",
|
||||
"DeleteNotificationMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?",
|
||||
"DeleteNotificationMessageText": "Ви впевнені, що хочете видалити сповіщення '{name}'?",
|
||||
"YesCancel": "Так, скасувати",
|
||||
"InstanceName": "Ім'я екземпляра",
|
||||
"Interval": "Інтервал",
|
||||
@@ -82,7 +82,7 @@
|
||||
"Uptime": "Час роботи",
|
||||
"URLBase": "URL-адреса",
|
||||
"DownloadClients": "Клієнти завантажувачів",
|
||||
"DownloadClientSettings": "Налаштування клієнта завантажувача",
|
||||
"DownloadClientSettings": "Налаштування клієнта завантаження",
|
||||
"DownloadClientStatusSingleClientHealthCheckMessage": "Завантаження клієнтів недоступне через помилки: {downloadClientNames}",
|
||||
"Edit": "Редагувати",
|
||||
"EditIndexer": "Редагувати індексатор",
|
||||
@@ -342,7 +342,7 @@
|
||||
"ApplyTagsHelpTextAdd": "Додати: додати теги до наявного списку тегів",
|
||||
"ApplyTagsHelpTextHowToApplyApplications": "Як застосувати теги до вибраних фільмів",
|
||||
"DeleteSelectedApplicationsMessageText": "Ви впевнені, що хочете видалити тег {0} ?",
|
||||
"DeleteSelectedDownloadClients": "Видалити клієнт завантаження",
|
||||
"DeleteSelectedDownloadClients": "Видалити клієнт(и) завантаження",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Ви впевнені, що хочете видалити тег {0} ?",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Як застосувати теги до вибраних індексаторів",
|
||||
"ApplyTagsHelpTextRemove": "Видалити: видалити введені теги",
|
||||
@@ -355,8 +355,8 @@
|
||||
"Year": "Рік",
|
||||
"UpdateAvailableHealthCheckMessage": "Доступне нове оновлення",
|
||||
"Genre": "Жанри",
|
||||
"ConnectionLostReconnect": "Radarr спробує підключитися автоматично, або ви можете натиснути перезавантажити нижче.",
|
||||
"ConnectionLostToBackend": "Radarr втратив зв’язок із бекендом, і його потрібно перезавантажити, щоб відновити функціональність.",
|
||||
"ConnectionLostReconnect": "{appName} спробує підключитися автоматично, або ви можете натиснути «Перезавантажити» нижче.",
|
||||
"ConnectionLostToBackend": "{appName} втратив з’єднання з серверною частиною, і його потрібно перезавантажити, щоб відновити роботу.",
|
||||
"DeleteAppProfileMessageText": "Ви впевнені, що хочете видалити цей профіль затримки?",
|
||||
"RecentChanges": "Останні зміни",
|
||||
"minutes": "Хвилин",
|
||||
@@ -365,7 +365,7 @@
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Списки недоступні через помилки: {notificationNames}",
|
||||
"AuthBasic": "Основний (спливаюче вікно браузера)",
|
||||
"AuthForm": "Форми (сторінка входу)",
|
||||
"DisabledForLocalAddresses": "Відключено для локальних адрес",
|
||||
"DisabledForLocalAddresses": "Вимкнено для локальних адрес",
|
||||
"None": "Жодного",
|
||||
"ResetAPIKeyMessageText": "Ви впевнені, що хочете скинути свій ключ API?",
|
||||
"AddConnection": "Додати Підключення",
|
||||
@@ -396,5 +396,19 @@
|
||||
"Artist": "Виконавець",
|
||||
"Yes": "Так",
|
||||
"AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.",
|
||||
"AppUpdatedVersion": "{appName} оновлено до версії `{version}`. Щоб отримати останні зміни, потрібно перезавантажити {appName}"
|
||||
"AppUpdatedVersion": "{appName} оновлено до версії `{version}`. Щоб отримати останні зміни, потрібно перезавантажити {appName}",
|
||||
"AuthenticationRequiredHelpText": "Змінити запити, для яких потрібна автентифікація. Не змінюйте, якщо не розумієте ризики.",
|
||||
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Чи використовувати налаштований макет вмісту qBittorrent, оригінальний макет із торрента чи завжди створювати вкладену папку (qBittorrent 4.3.2+)",
|
||||
"Category": "Категорія",
|
||||
"BlackholeFolderHelpText": "Папка, у якій {appName} зберігатиме файл {extension}",
|
||||
"DownloadClientFloodSettingsAdditionalTags": "Додаткові теги",
|
||||
"DownloadClientDownloadStationSettingsDirectoryHelpText": "Додаткова спільна папка для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Download Station",
|
||||
"DownloadClientFloodSettingsUrlBaseHelpText": "Додає префікс до API Flood, наприклад {url}",
|
||||
"DownloadClientFreeboxSettingsApiUrl": "API URL",
|
||||
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Спочатку завантажте першу та останню частини (qBittorrent 4.1.0+)",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Завантаження в послідовному порядку (qBittorrent 4.1.0+)",
|
||||
"DownloadClientQbittorrentSettingsUseSslHelpText": "Використовувати безпечне з'єднання. Дивіться параметри -> Web UI -> 'Use HTTPS instead of HTTP' в qBittorrent.",
|
||||
"DownloadClientSettingsInitialStateHelpText": "Початковий стан для торрентів, доданих до {clientName}",
|
||||
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Підтвердити новий пароль",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "Додаткове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Aria2"
|
||||
}
|
||||
|
||||
@@ -604,5 +604,6 @@
|
||||
"DownloadClientQbittorrentSettingsContentLayout": "内容布局",
|
||||
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "是否使用 qBittorrent 配置的内容布局,使用种子的原始布局或始终创建子文件夹(qBittorrent 4.3.2+)",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置",
|
||||
"ManageClients": "管理客户端"
|
||||
"ManageClients": "管理客户端",
|
||||
"CustomFilter": "自定义过滤器"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp.Xml" Version="1.0.0" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Dapper" Version="2.0.151" />
|
||||
<PackageReference Include="MailKit" Version="3.6.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.25" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.29" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.6" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Update.History.Events;
|
||||
@@ -18,13 +19,15 @@ namespace NzbDrone.Core.Update.History
|
||||
{
|
||||
private readonly IUpdateHistoryRepository _repository;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly Logger _logger;
|
||||
private Version _prevVersion;
|
||||
|
||||
public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, Logger logger)
|
||||
public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, IConfigFileProvider configFileProvider, Logger logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_configFileProvider = configFileProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -58,7 +61,7 @@ namespace NzbDrone.Core.Update.History
|
||||
|
||||
public void Handle(ApplicationStartedEvent message)
|
||||
{
|
||||
if (BuildInfo.Version.Major == 10)
|
||||
if (BuildInfo.Version.Major == 10 || !_configFileProvider.LogDbEnabled)
|
||||
{
|
||||
// Don't save dev versions, they change constantly
|
||||
return;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Update
|
||||
{
|
||||
var branch = _configFileProvider.Branch;
|
||||
var version = BuildInfo.Version;
|
||||
var prevVersion = _updateHistoryService.PreviouslyInstalled();
|
||||
var prevVersion = _configFileProvider.LogDbEnabled ? _updateHistoryService.PreviouslyInstalled() : null;
|
||||
return _updatePackageProvider.GetRecentUpdates(branch, version, prevVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using NzbDrone.Common;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Download;
|
||||
@@ -46,6 +47,11 @@ namespace NzbDrone.App.Test
|
||||
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance<IBroadcastSignalRMessage>(new Mock<IBroadcastSignalRMessage>().Object);
|
||||
container.RegisterInstance<IOptions<PostgresOptions>>(new Mock<IOptions<PostgresOptions>>().Object);
|
||||
container.RegisterInstance<IOptions<AuthOptions>>(new Mock<IOptions<AuthOptions>>().Object);
|
||||
container.RegisterInstance<IOptions<AppOptions>>(new Mock<IOptions<AppOptions>>().Object);
|
||||
container.RegisterInstance<IOptions<ServerOptions>>(new Mock<IOptions<ServerOptions>>().Object);
|
||||
container.RegisterInstance<IOptions<UpdateOptions>>(new Mock<IOptions<UpdateOptions>>().Object);
|
||||
container.RegisterInstance<IOptions<LogOptions>>(new Mock<IOptions<LogOptions>>().Object);
|
||||
|
||||
_container = container.GetServiceProvider();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user