Compare commits

..

42 Commits

Author SHA1 Message Date
Weblate
2c5f2187c8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translation: Servarr/Prowlarr
2024-05-10 14:05:29 +03:00
Bogdan
401ef88971 Refactor PasswordInput to use type password 2024-05-10 13:59:52 +03:00
Bogdan
4fb3754048 Fixed: Text color for inputs on login page 2024-05-09 23:40:50 +03:00
Mark McDowall
596efe8fb0 New: Dark theme for login screen
(cherry picked from commit cae134ec7b331d1c906343716472f3d043614b2c)
2024-05-09 23:40:49 +03:00
Bogdan
076a4f2574 Fix class name for AppIndexerMapRepository 2024-05-09 23:15:05 +03:00
Servarr
9561371a47 Automated API Docs update 2024-05-08 03:20:28 +03:00
Mark McDowall
16254cf5f9 New: Option to select download client when multiple of the same type are configured
(cherry picked from commit 07f0fbf9a51d54e44681fd0f74df4e048bff561a)
2024-05-08 03:14:17 +03:00
Jared
649a03e5a0 New: Config file setting to disable log database (#2123)
Co-authored-by: sillock1 <jprest97@gmail.com>
2024-05-07 19:52:02 +03:00
Bogdan
dd21d9b521 Fixed: Allow decimals for Seed Ratio 2024-05-07 00:11:20 +03:00
Bogdan
68b895d2ad Fixed: Don't share settings for same cached definition in CardigannRequestGenerator 2024-05-06 18:22:54 +03:00
Weblate
634016ae1b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translation: Servarr/Prowlarr
2024-05-05 13:12:04 +03:00
Mark McDowall
83c6751847 Forward X-Forwarded-Host header
(cherry picked from commit 3fbe4361386e9fb8dafdf82ad9f00f02bec746cc)
2024-05-05 12:29:22 +03:00
Jared
04bb0c51b1 New: Optionally use Environment Variables for settings in config.xml
(cherry picked from commit d051dac12c8b797761a0d1f3b4aa84cff47ed13d)
2024-05-05 11:39:55 +03:00
Bogdan
d2e9621de9 Bump version to 1.17.2 2024-05-05 11:38:40 +03:00
Bogdan
cb673ddc42 New: Host column in history and more info 2024-05-02 22:35:20 +03:00
Bogdan
440618f2b6 Fixed: Initialize databases after app folder migrations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-05-02 00:21:01 +03:00
Bruno Garcia
ae79d45664 Update Sentry SDK add features
Co-authored-by: Stefan Jandl <reg@bitfox.at>
(cherry picked from commit 6377c688fc7b35749d608bf62796446bb5bcb11b)
2024-05-02 00:07:58 +03:00
Bogdan
1877ccb513 Update Pull Request Labeler config for v5 2024-04-29 14:35:43 +03:00
Bogdan
b3098f2e4c Use newer Node.js task for in pipelines 2024-04-29 14:32:46 +03:00
Bogdan
3e0af062c1 Parameter binding for API requests 2024-04-29 01:24:57 +03:00
Bogdan
858f85c50d Fix translations for proxy validation 2024-04-28 23:49:59 +03:00
Uruk
938848be65 (ci): update action version 2024-04-28 11:42:07 -05:00
Bogdan
615f5899cc Fixed: (TorrentDay) Update base urls and MST
Co-authored-by: garfield69 <garfield69@outlook.com>
2024-04-28 14:57:30 +03:00
Mark McDowall
5a6b1313e8 Validate that folders in paths don't start or end with a space
(cherry picked from commit 316b5cbf75b45ef9a25f96ce1f2fbed25ad94296)
2024-04-28 13:16:08 +03:00
Mark McDowall
ab7debb34b Improve paths longer than 256 on Windows failing to hardlink
(cherry picked from commit a97fbcc40a6247bf59678425cf460588fd4dbecd)
2024-04-28 13:07:52 +03:00
Bogdan
eee21de795 Fixed: Handle download redirects to magnet links 2024-04-28 13:05:13 +03:00
Bogdan
15fabbe7d0 Bump version to 1.17.1 2024-04-28 12:50:04 +03:00
Weblate
6aef48c6e7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: maodun96 <435795439@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-04-27 21:15:01 +03:00
Bogdan
b29bc923fc Fixed: Don't reset sorting, columns and selected filter on clear releases
Fixes #2112
2024-04-25 18:46:37 +03:00
Taloth Saldono
b223e9b0cc Should not empty install folder, MirrorFolder will take care of it.
(cherry picked from commit cd450a44bf34da3720f4a1551acd2f0ce2aa313d)
2024-04-25 15:09:23 +03:00
Bogdan
77a982a7da Fixed: Retrying download on not suppressed HTTP errors 2024-04-25 14:50:30 +03:00
Bogdan
ab3dc765b4 Database corruption message linking to wiki 2024-04-25 11:18:33 +03:00
Bogdan
0261201360 Fixed: (GazelleGames) Update categories
Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2024-04-24 10:53:14 +03:00
Bogdan
1da3954879 New: (GazelleGames) Freeleech only option 2024-04-24 10:48:17 +03:00
Bogdan
742dd5ff54 Update BTN tests 2024-04-24 10:22:05 +03:00
Bogdan
a85406e3b7 Fixed: (BroadcasTheNet) Append wildcard when searching for single episodes
Some results include the episode title after SxxEyy and the wildcard helps to find those too.
2024-04-24 09:48:07 +03:00
Bogdan
73cdaf3d44 Bump NUnit and Microsoft.NET.Test.Sdk 2024-04-22 08:07:21 +03:00
Bogdan
e26fa2dbf4 Fixed: (Anidex) Support season and episode for TV searches 2024-04-21 19:22:21 +03:00
Bogdan
64be68a22d Bump dotnet to 6.0.29 2024-04-21 13:16:51 +03:00
Bogdan
478a185968 Convert createDimensionsSelector to typescript 2024-04-21 13:08:56 +03:00
Bogdan
4ff5d11a03 Bump frontend dependencies 2024-04-21 13:05:55 +03:00
Bogdan
6000952b76 Bump version to 1.17.0 2024-04-20 09:32:44 +03:00
119 changed files with 4135 additions and 2645 deletions

28
.github/labeler.yml vendored
View File

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

View File

@@ -9,4 +9,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5

View File

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

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.16.2'
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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
.input {
composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily;
}

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
enum DownloadProtocol {
Unknown = 'unknown',
Usenet = 'usenet',
Torrent = 'torrent',
}
export default DownloadProtocol;

View File

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

View File

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

View File

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

View File

@@ -224,6 +224,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
name="seedRatio"
value={seedRatio}
helpText={translate('SeedRatioHelpText')}
isFloat={true}
onChange={onInputChange}
/>
</FormGroup>

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
.downloadClient {
display: flex;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid var(--borderColor);
}

View File

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

View File

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

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

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

View 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
)}
>
&nbsp;
</span>
) : (
value
)}
</Link>
);
}
export default OverrideMatchData;

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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 }) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,10 @@ function formatBytes(input) {
return '';
}
return filesize(size, {
return `${filesize(size, {
base: 2,
round: 1
});
})}`;
}
export default formatBytes;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,10 +29,10 @@
"View": "Vista",
"Updates": "Actualizaciones",
"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 \"App Translocation\".",
"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",
@@ -167,7 +167,7 @@
"URLBase": "URL base",
"Uptime": "Tiempo de actividad",
"UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización",
"UpdateMechanismHelpText": "Usar el actualizador 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",
@@ -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 usa 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",
@@ -749,5 +749,16 @@
"IndexerSettingsVipExpiration": "Expiración del VIP",
"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"
"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"
}

View File

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

View File

@@ -749,5 +749,16 @@
"IndexerMTeamTpSettingsApiKeyHelpText": "Chave API do Site (Encontrada no Painel de Controle do Usuário => Segurança => Laboratório)",
"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"
"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"
}

View File

@@ -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",
@@ -64,8 +64,8 @@
"Edit": "Düzenle",
"CustomFilters": "Özel Filtreler",
"ConnectSettingsSummary": "Bildirimler, medya sunucularına/oynatıcılara bağlantılar ve özel komut kodları",
"Analytics": "Analiz",
"All": "Herşey",
"Analytics": "Analitik",
"All": "Hepsi",
"Added": "Eklendi",
"Add": "Ekle",
"Branch": "Şube",
@@ -85,7 +85,7 @@
"Backups": "Yedeklemeler",
"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,10 +94,10 @@
"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",
@@ -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": "Y",
"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. Bu, tarayıcınızla ilgili bilgileri, kullandığınız {appName} Web arayüz 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.",
"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",
@@ -146,11 +146,11 @@
"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",
@@ -305,32 +305,32 @@
"UnableToLoadGeneralSettings": "Genel ayarlar yüklenemiyor",
"Automatic": "Otomatik",
"AutomaticSearch": "Otomatik Arama",
"Backup": "Yedek",
"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",
@@ -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ıılır Penceresi)",
"AuthForm": "Formlar (Giriş Sayfası)",
@@ -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",
@@ -420,7 +420,7 @@
"Database": "Veri tabanı",
"DefaultNameCopiedProfile": "{name} - Kopyala",
"DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?",
"DeleteSelectedIndexersMessageText": "Seçilen {count} dizin oluşturucuyu 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",
@@ -431,8 +431,69 @@
"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 Token'ı",
"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)"
"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"
}

View File

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

View File

@@ -604,5 +604,6 @@
"DownloadClientQbittorrentSettingsContentLayout": "内容布局",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "是否使用 qBittorrent 配置的内容布局使用种子的原始布局或始终创建子文件夹qBittorrent 4.3.2+",
"DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置",
"ManageClients": "管理客户端"
"ManageClients": "管理客户端",
"CustomFilter": "自定义过滤器"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,9 +22,9 @@ using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore.Extensions;
using Prowlarr.Http.ClientSchema;
using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions;
namespace NzbDrone.Host
@@ -94,10 +94,24 @@ namespace NzbDrone.Host
.AddStartupContext(startupContext)
.Resolve<UtilityModeRouter>()
.Route(appMode);
if (config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true))
{
c.AddLogDatabase();
}
else
{
c.AddDummyLogDatabase();
}
})
.ConfigureServices(services =>
{
services.Configure<PostgresOptions>(config.GetSection("Prowlarr:Postgres"));
services.Configure<AppOptions>(config.GetSection("Prowlarr:App"));
services.Configure<AuthOptions>(config.GetSection("Prowlarr:Auth"));
services.Configure<ServerOptions>(config.GetSection("Prowlarr:Server"));
services.Configure<LogOptions>(config.GetSection("Prowlarr:Log"));
services.Configure<UpdateOptions>(config.GetSection("Prowlarr:Update"));
}).Build();
break;
@@ -125,12 +139,13 @@ namespace NzbDrone.Host
{
var config = GetConfiguration(context);
var bindAddress = config.GetValue(nameof(ConfigFileProvider.BindAddress), "*");
var port = config.GetValue(nameof(ConfigFileProvider.Port), ConfigFileProvider.DEFAULT_PORT);
var sslPort = config.GetValue(nameof(ConfigFileProvider.SslPort), ConfigFileProvider.DEFAULT_SSL_PORT);
var enableSsl = config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
var sslCertPath = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
var sslCertPassword = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
var bindAddress = config.GetValue<string>($"Prowlarr:Server:{nameof(ServerOptions.BindAddress)}") ?? config.GetValue(nameof(ConfigFileProvider.BindAddress), "*");
var port = config.GetValue<int?>($"Prowlarr:Server:{nameof(ServerOptions.Port)}") ?? config.GetValue(nameof(ConfigFileProvider.Port), ConfigFileProvider.DEFAULT_PORT);
var sslPort = config.GetValue<int?>($"Prowlarr:Server:{nameof(ServerOptions.SslPort)}") ?? config.GetValue(nameof(ConfigFileProvider.SslPort), ConfigFileProvider.DEFAULT_SSL_PORT);
var enableSsl = config.GetValue<bool?>($"Prowlarr:Server:{nameof(ServerOptions.EnableSsl)}") ?? config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
var sslCertPath = config.GetValue<string>($"Prowlarr:Server:{nameof(ServerOptions.SslCertPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
var sslCertPassword = config.GetValue<string>($"Prowlarr:Server:{nameof(ServerOptions.SslCertPassword)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
var logDbEnabled = config.GetValue<bool?>($"Prowlarr:Log:{nameof(LogOptions.DbEnabled)}") ?? config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true);
var urls = new List<string> { BuildUrl("http", bindAddress, port) };
@@ -149,11 +164,23 @@ namespace NzbDrone.Host
.AddDatabase()
.AddStartupContext(context);
SchemaBuilder.Initialize(c);
if (logDbEnabled)
{
c.AddLogDatabase();
}
else
{
c.AddDummyLogDatabase();
}
})
.ConfigureServices(services =>
{
services.Configure<PostgresOptions>(config.GetSection("Prowlarr:Postgres"));
services.Configure<AppOptions>(config.GetSection("Prowlarr:App"));
services.Configure<AuthOptions>(config.GetSection("Prowlarr:Auth"));
services.Configure<ServerOptions>(config.GetSection("Prowlarr:Server"));
services.Configure<LogOptions>(config.GetSection("Prowlarr:Log"));
services.Configure<UpdateOptions>(config.GetSection("Prowlarr:Update"));
services.Configure<FormOptions>(x =>
{
//Double the default multipart body length from 128 MB to 256 MB

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using DryIoc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
@@ -26,6 +27,7 @@ using NzbDrone.SignalR;
using Prowlarr.Api.V1.System;
using Prowlarr.Http;
using Prowlarr.Http.Authentication;
using Prowlarr.Http.ClientSchema;
using Prowlarr.Http.ErrorManagement;
using Prowlarr.Http.Frontend;
using Prowlarr.Http.Middleware;
@@ -56,7 +58,7 @@ namespace NzbDrone.Host
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
@@ -208,6 +210,7 @@ namespace NzbDrone.Host
}
public void Configure(IApplicationBuilder app,
IContainer container,
IStartupContext startupContext,
Lazy<IMainDatabase> mainDatabaseFactory,
Lazy<ILogDatabase> logDatabaseFactory,
@@ -235,9 +238,14 @@ namespace NzbDrone.Host
// instantiate the databases to initialize/migrate them
_ = mainDatabaseFactory.Value;
_ = logDatabaseFactory.Value;
dbTarget.Register();
if (configFileProvider.LogDbEnabled)
{
_ = logDatabaseFactory.Value;
dbTarget.Register();
}
SchemaBuilder.Initialize(container);
if (OsInfo.IsNotWindows)
{

View File

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

View File

@@ -7,7 +7,7 @@
<PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NLog" Version="5.2.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
</ItemGroup>

View File

@@ -122,9 +122,6 @@ namespace NzbDrone.Update.UpdateEngine
try
{
_logger.Info("Emptying installation folder");
_diskProvider.EmptyFolder(installationFolder);
_logger.Info("Copying new files to target folder");
_diskTransferService.MirrorFolder(_appFolderInfo.GetUpdatePackageFolder(), installationFolder);

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