mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-16 16:04:14 -04:00
Compare commits
106 Commits
v0.3.26.25
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e739a93a2a | ||
|
|
9005860899 | ||
|
|
c67f67109e | ||
|
|
51b9744e25 | ||
|
|
334d824633 | ||
|
|
ae01387ca9 | ||
|
|
4eb13e0938 | ||
|
|
6dbb826f2f | ||
|
|
52dfa57dd7 | ||
|
|
f354b3bc47 | ||
|
|
2d9e6788e6 | ||
|
|
0d121fe9c0 | ||
|
|
892c34fe35 | ||
|
|
24f6007594 | ||
|
|
5028ed4027 | ||
|
|
05f303436b | ||
|
|
5635de96a8 | ||
|
|
ce59f32023 | ||
|
|
6d675a5207 | ||
|
|
b093b23900 | ||
|
|
884ac2cb6f | ||
|
|
295a6c4255 | ||
|
|
74a59d5790 | ||
|
|
ae23e5f187 | ||
|
|
ba2add0d54 | ||
|
|
b6ebeb31c8 | ||
|
|
b8bd645560 | ||
|
|
e0d904fa69 | ||
|
|
cb532caca4 | ||
|
|
e1af8ad37f | ||
|
|
c4f30da648 | ||
|
|
b83a760873 | ||
|
|
22ab50f76d | ||
|
|
66758ca006 | ||
|
|
e7d7bc79f4 | ||
|
|
cfccb4f9c3 | ||
|
|
9312f17041 | ||
|
|
8192c22910 | ||
|
|
0b1d6b677a | ||
|
|
d666df0189 | ||
|
|
10d8f345c1 | ||
|
|
fb720b8714 | ||
|
|
e8131b5791 | ||
|
|
4f793f6b93 | ||
|
|
4215c21c94 | ||
|
|
6913789adc | ||
|
|
09e0c40792 | ||
|
|
baff805551 | ||
|
|
c885fe43cd | ||
|
|
464a777722 | ||
|
|
89e5999c85 | ||
|
|
b6fa332550 | ||
|
|
05f262dc0a | ||
|
|
699b765ee9 | ||
|
|
84beba2383 | ||
|
|
62eceb9148 | ||
|
|
f46070d4b0 | ||
|
|
73979c416a | ||
|
|
348e8f9c27 | ||
|
|
38bdb5a75d | ||
|
|
5e4c51e2f7 | ||
|
|
99a65246a9 | ||
|
|
598ce9a9d2 | ||
|
|
42d6b9e703 | ||
|
|
8f595838aa | ||
|
|
3d9d7d3582 | ||
|
|
77cf28bd78 | ||
|
|
2fb1b8af20 | ||
|
|
af1f389f8e | ||
|
|
b5334da253 | ||
|
|
68b3904382 | ||
|
|
c8b09b9e29 | ||
|
|
d910fc42ab | ||
|
|
a6db8bfe0e | ||
|
|
2033d7e411 | ||
|
|
4a04e54ceb | ||
|
|
d57a9ab9b0 | ||
|
|
d333204194 | ||
|
|
c3676f8d33 | ||
|
|
932356be61 | ||
|
|
5b1b2a2d67 | ||
|
|
c362e8c467 | ||
|
|
67c00a8cc7 | ||
|
|
27a086dfff | ||
|
|
8ee0df9c65 | ||
|
|
da30b55902 | ||
|
|
c7226fc85f | ||
|
|
84f22dbadc | ||
|
|
06a53ef9ca | ||
|
|
b5ef0cda1e | ||
|
|
1b1290efac | ||
|
|
dcbc3ea3f8 | ||
|
|
9a7b2cb818 | ||
|
|
f9cba39f0a | ||
|
|
6b6ff4fe76 | ||
|
|
05d0fe2da6 | ||
|
|
7aab2b49e2 | ||
|
|
8887df92ed | ||
|
|
9ee651d6c0 | ||
|
|
5544e169a6 | ||
|
|
11d83165e5 | ||
|
|
9e6d1c581c | ||
|
|
66e20a0aec | ||
|
|
e639b36283 | ||
|
|
c9f4fb141f | ||
|
|
29a43fc2fd |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -120,6 +120,7 @@ _artifacts
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_temp*
|
||||
*.Result.xml
|
||||
coverage*.xml
|
||||
coverage*.json
|
||||
|
||||
@@ -9,18 +9,18 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.3.26'
|
||||
majorVersion: '0.4.6'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.421'
|
||||
dotnetVersion: '6.0.427'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
macImage: 'macOS-13'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -1102,7 +1102,7 @@ stages:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@1
|
||||
- task: SonarCloudPrepare@2
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
@@ -1114,7 +1114,7 @@ stages:
|
||||
cliProjectName: 'ReadarrUI'
|
||||
cliProjectVersion: '$(readarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@1
|
||||
- task: SonarCloudAnalyze@2
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
@@ -1190,7 +1190,7 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@1
|
||||
- task: SonarCloudPrepare@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
@@ -1208,21 +1208,16 @@ stages:
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
- task: SonarCloudAnalyze@2
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@4
|
||||
- task: reportgenerator@5.3.11
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: Publish Coverage Report
|
||||
inputs:
|
||||
codeCoverageTool: 'cobertura'
|
||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||
reportDirectory: './CoverageResults/combined/'
|
||||
publishCodeCoverageResults: true
|
||||
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
|
||||
14
docs.sh
14
docs.sh
@@ -1,3 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
@@ -21,15 +25,21 @@ slnFile=src/Readarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Readarr.Console.dll
|
||||
else
|
||||
application=Readarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/Readarr.console.dll" v1 &
|
||||
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
|
||||
@@ -26,6 +26,7 @@ module.exports = (env) => {
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
@@ -67,7 +68,7 @@ module.exports = (env) => {
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name]-[contenthash].js',
|
||||
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
@@ -92,7 +93,7 @@ module.exports = (env) => {
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css',
|
||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
||||
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -202,7 +203,7 @@ module.exports = (env) => {
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
@@ -247,7 +247,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={UpdatesConnector}
|
||||
component={Updates}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AuthorsAppState from './AuthorsAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
@@ -35,10 +36,24 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
authors: AuthorsAppState;
|
||||
commands: CommandAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
@@ -7,13 +8,16 @@ import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
@@ -33,11 +37,12 @@ export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
notifications: NotificationAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
||||
13
frontend/src/App/State/SystemAppState.ts
Normal file
13
frontend/src/App/State/SystemAppState.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
||||
import styles from './AuthorIndexOverviewInfo.css';
|
||||
|
||||
@@ -76,9 +77,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
title: translate('QualityProfile'),
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -235,12 +235,12 @@ class AuthorIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -209,7 +209,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile.name}
|
||||
{qualityProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -220,7 +220,7 @@ class AuthorIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{metadataProfile.name}
|
||||
{metadataProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
||||
import styles from './BookIndexOverviewInfo.css';
|
||||
|
||||
@@ -71,9 +72,9 @@ function getInfoRowProps(row, props) {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
title: translate('QualityProfile'),
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
|
||||
@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{showQualityProfile && !!qualityProfile?.name ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
|
||||
@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile.name}
|
||||
{qualityProfile?.name ?? ''}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
font-family: $passwordFamily;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type="password"
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
className: styles.input
|
||||
...TextInput.props
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
&.isDisabled {
|
||||
color: var(--disabledColor);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||
import SignalRConnector from 'Components/SignalRConnector';
|
||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||
import PageHeader from './Header/PageHeader';
|
||||
import PageSidebar from './Sidebar/PageSidebar';
|
||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
||||
isSmallScreen,
|
||||
isSidebarVisible,
|
||||
enableColorImpairedMode,
|
||||
authenticationEnabled,
|
||||
onSidebarToggle,
|
||||
onSidebarVisibleChange
|
||||
} = this.props;
|
||||
@@ -108,6 +110,10 @@ class Page extends Component {
|
||||
isOpen={this.state.isConnectionLostModalOpen}
|
||||
onModalClose={this.onConnectionLostModalClose}
|
||||
/>
|
||||
|
||||
<AuthenticationRequiredModal
|
||||
isOpen={!authenticationEnabled}
|
||||
/>
|
||||
</div>
|
||||
</ColorImpairedContext.Provider>
|
||||
);
|
||||
@@ -123,6 +129,7 @@ Page.propTypes = {
|
||||
isUpdated: PropTypes.bool.isRequired,
|
||||
isDisconnected: PropTypes.bool.isRequired,
|
||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||
authenticationEnabled: PropTypes.bool.isRequired,
|
||||
onResize: PropTypes.func.isRequired,
|
||||
onSidebarToggle: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import ErrorPage from './ErrorPage';
|
||||
import LoadingPage from './LoadingPage';
|
||||
import Page from './Page';
|
||||
@@ -153,18 +154,21 @@ function createMapStateToProps() {
|
||||
selectErrors,
|
||||
selectAppProps,
|
||||
createDimensionsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
(
|
||||
enableColorImpairedMode,
|
||||
isPopulated,
|
||||
errors,
|
||||
app,
|
||||
dimensions
|
||||
dimensions,
|
||||
systemStatus
|
||||
) => {
|
||||
return {
|
||||
...app,
|
||||
...errors,
|
||||
isPopulated,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||
enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ class SignalRConnector extends Component {
|
||||
handleWantedCutoff = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({
|
||||
section: 'cutoffUnmet',
|
||||
section: 'wanted.cutoffUnmet',
|
||||
updateOnly: true,
|
||||
...body.resource
|
||||
});
|
||||
@@ -263,7 +263,7 @@ class SignalRConnector extends Component {
|
||||
handleWantedMissing = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
this.props.dispatchUpdateItem({
|
||||
section: 'missing',
|
||||
section: 'wanted.missing',
|
||||
updateOnly: true,
|
||||
...body.resource
|
||||
});
|
||||
|
||||
@@ -25,14 +25,3 @@
|
||||
font-family: 'Ubuntu Mono';
|
||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* text-security-disc
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: 'text-security-disc';
|
||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
||||
@@ -0,0 +1,5 @@
|
||||
.authRequiredAlert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'input': string;
|
||||
'authRequiredAlert': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
170
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
170
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
dispatchFetchStatus
|
||||
} = props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && didMount.current) {
|
||||
dispatchFetchStatus();
|
||||
}
|
||||
|
||||
didMount.current = true;
|
||||
}, [isSaving, dispatchFetchStatus]);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('AuthenticationRequired')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#forced-authentication"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModalContent;
|
||||
@@ -0,0 +1,86 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(sectionSettings) => {
|
||||
return {
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchClearPendingChanges: clearPendingChanges,
|
||||
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||
dispatchFetchStatus: fetchStatus
|
||||
};
|
||||
|
||||
class AuthenticationRequiredModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveGeneralSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchFetchGeneralSettings,
|
||||
dispatchSetGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AuthenticationRequiredModalContent
|
||||
{...otherProps}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
@@ -7,7 +8,7 @@ import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddNewAuthorSearchResultConnector from './Author/AddNewAuthorSearchResultConnector';
|
||||
@@ -127,9 +128,16 @@ class AddNewItem extends Component {
|
||||
!isFetching && !!error ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Failed to load search results, please try again.
|
||||
{translate('FailedLoadingSearchResults')}
|
||||
</div>
|
||||
|
||||
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
|
||||
|
||||
<div>
|
||||
<Link to="https://wiki.servarr.com/readarr/troubleshooting#invalid-response-received-from-metadata-api">
|
||||
{translate('WhySearchesCouldBeFailing')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
|
||||
@@ -25,3 +25,8 @@
|
||||
border-radius: 4px;
|
||||
background-color: var(--cardCenterBackgroundColor);
|
||||
}
|
||||
|
||||
.customFormats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
interface CssExports {
|
||||
'addSpecification': string;
|
||||
'center': string;
|
||||
'customFormats': string;
|
||||
'deleteButton': string;
|
||||
'rightButtons': string;
|
||||
}
|
||||
|
||||
@@ -11,16 +11,69 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const authenticationMethodOptions = [
|
||||
{ key: 'none', value: 'None' },
|
||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||
export const authenticationMethodOptions = [
|
||||
{
|
||||
key: 'none',
|
||||
get value() {
|
||||
return translate('None');
|
||||
},
|
||||
isDisabled: true
|
||||
},
|
||||
{
|
||||
key: 'external',
|
||||
get value() {
|
||||
return translate('External');
|
||||
},
|
||||
isHidden: true
|
||||
},
|
||||
{
|
||||
key: 'basic',
|
||||
get value() {
|
||||
return translate('AuthBasic');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'forms',
|
||||
get value() {
|
||||
return translate('AuthForm');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const authenticationRequiredOptions = [
|
||||
{
|
||||
key: 'enabled',
|
||||
get value() {
|
||||
return translate('Enabled');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disabledForLocalAddresses',
|
||||
get value() {
|
||||
return translate('DisabledForLocalAddresses');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const certificateValidationOptions = [
|
||||
{ key: 'enabled', value: 'Enabled' },
|
||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
||||
{ key: 'disabled', value: 'Disabled' }
|
||||
{
|
||||
key: 'enabled',
|
||||
get value() {
|
||||
return translate('Enabled');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disabledForLocalAddresses',
|
||||
get value() {
|
||||
return translate('DisabledForLocalAddresses');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disabled',
|
||||
get value() {
|
||||
return translate('Disabled');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class SecuritySettings extends Component {
|
||||
@@ -68,8 +121,10 @@ class SecuritySettings extends Component {
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
apiKey,
|
||||
certificateValidation
|
||||
} = settings;
|
||||
@@ -79,26 +134,40 @@ class SecuritySettings extends Component {
|
||||
return (
|
||||
<FieldSet legend={translate('Security')}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Authentication')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
helpTextWarning={translate('AuthenticationRequiredWarning')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Username')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText={translate('AuthenticationRequiredHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Username')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
@@ -106,15 +175,14 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled &&
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Password')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('Password')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
@@ -122,19 +190,33 @@ class SecuritySettings extends Component {
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('APIKey')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('ApiKey')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="apiKey"
|
||||
readOnly={true}
|
||||
helpTextWarning={translate('ApiKeyHelpTextWarning')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
buttons={[
|
||||
<ClipboardButton
|
||||
key="copy"
|
||||
@@ -160,9 +242,7 @@ class SecuritySettings extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('CertificateValidation')}
|
||||
</FormLabel>
|
||||
<FormLabel>{translate('CertificateValidation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
|
||||
@@ -18,7 +18,6 @@ function UpdateSettings(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
settings,
|
||||
isWindows,
|
||||
packageUpdateMechanism,
|
||||
onInputChange
|
||||
} = props;
|
||||
@@ -44,10 +43,10 @@ function UpdateSettings(props) {
|
||||
value: titleCase(packageUpdateMechanism)
|
||||
});
|
||||
} else {
|
||||
updateOptions.push({ key: 'builtIn', value: 'Built-In' });
|
||||
updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') });
|
||||
}
|
||||
|
||||
updateOptions.push({ key: 'script', value: 'Script' });
|
||||
updateOptions.push({ key: 'script', value: translate('Script') });
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Updates')}>
|
||||
@@ -60,8 +59,8 @@ function UpdateSettings(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.AUTO_COMPLETE}
|
||||
name="branch"
|
||||
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
|
||||
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
||||
helpLink="https://wiki.servarr.com/readarr/settings#updates"
|
||||
{...branch}
|
||||
values={branchValues}
|
||||
onChange={onInputChange}
|
||||
@@ -69,62 +68,59 @@ function UpdateSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
!isWindows &&
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
<div>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="updateAutomatically"
|
||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
|
||||
onChange={onInputChange}
|
||||
{...updateAutomatically}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/readarr/settings#updates"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-i-update-my-readarr"
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
updateMechanism.value === 'script' &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="updateScriptPath"
|
||||
helpText={translate('UpdateScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...updateScriptPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import TagListConnector from 'Components/TagListConnector';
|
||||
import { createMetadataProfileSelectorForHook } from 'Store/Selectors/createMetadataProfileSelector';
|
||||
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ManageImportListsModalRow.css';
|
||||
|
||||
interface ManageImportListsModalRowProps {
|
||||
@@ -70,7 +71,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.qualityProfileId}>
|
||||
{qualityProfile?.name ?? 'None'}
|
||||
{qualityProfile?.name ?? translate('None')}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.metadataProfileId}>
|
||||
@@ -82,7 +83,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.enableAutomaticAdd}>
|
||||
{enableAutomaticAdd ? 'Yes' : 'No'}
|
||||
{enableAutomaticAdd ? translate('Yes') : translate('No')}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.tags}>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -15,11 +14,11 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.namingExamples,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, examples, sectionSettings) => {
|
||||
(advancedSettings, namingExamples, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
examples: examples.item,
|
||||
examplesPopulated: !_.isEmpty(examples.item),
|
||||
examples: namingExamples.item,
|
||||
examplesPopulated: namingExamples.isPopulated,
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,12 +75,12 @@ class RootFolder extends Component {
|
||||
{path}
|
||||
</Label>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{qualityProfile.name}
|
||||
<Label kind={qualityProfile?.name ? kinds.SUCCESS : kinds.DANGER}>
|
||||
{qualityProfile?.name || translate('None')}
|
||||
</Label>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{metadataProfile.name}
|
||||
<Label kind={metadataProfile?.name ? kinds.SUCCESS : kinds.DANGER}>
|
||||
{metadataProfile?.name || translate('None')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
class UpdateChanges extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
changes
|
||||
} = this.props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{
|
||||
changes.map((change, index) => {
|
||||
const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
|
||||
return `[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(1)})`;
|
||||
});
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UpdateChanges.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
changes: PropTypes.arrayOf(PropTypes.string)
|
||||
};
|
||||
|
||||
export default UpdateChanges;
|
||||
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal file
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
interface UpdateChangesProps {
|
||||
title: string;
|
||||
changes: string[];
|
||||
}
|
||||
|
||||
function UpdateChanges(props: UpdateChangesProps) {
|
||||
const { title, changes } = props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueChanges = [...new Set(changes)];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{uniqueChanges.map((change, index) => {
|
||||
const checkChange = change.replace(
|
||||
/#\d{4,5}\b/g,
|
||||
(match) =>
|
||||
`[${match}](https://github.com/Readarr/Readarr/issues/${match.substring(
|
||||
1
|
||||
)})`
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdateChanges;
|
||||
@@ -1,252 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
class Updates extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentVersion,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
isInstallingUpdate,
|
||||
updateMechanism,
|
||||
isDocker,
|
||||
updateMechanismMessage,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onInstallLatestPress
|
||||
} = this.props;
|
||||
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const externalUpdaterPrefix = 'Unable to update Readarr directly,';
|
||||
const externalUpdaterMessages = {
|
||||
external: 'Readarr is configured to use an external update mechanism',
|
||||
apt: 'use apt to install the update',
|
||||
docker: 'update the docker container to receive the update'
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{
|
||||
!isPopulated && !hasError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
noUpdates &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoUpdatesAreAvailable')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
{
|
||||
(updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ?
|
||||
<SpinnerButton
|
||||
className={styles.updateAvailable}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={onInstallLatestPress}
|
||||
>
|
||||
Install Latest
|
||||
</SpinnerButton> :
|
||||
|
||||
<Fragment>
|
||||
<Icon
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
noUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
The latest version of Readarr is already installed
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdates &&
|
||||
<div>
|
||||
{
|
||||
items.map((update) => {
|
||||
const hasChanges = !!update.changes;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={update.version}
|
||||
className={styles.update}
|
||||
>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{
|
||||
update.branch === 'master' ?
|
||||
null :
|
||||
<Label
|
||||
className={styles.label}
|
||||
>
|
||||
{update.branch}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
update.version === currentVersion ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Currently Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
update.version !== currentVersion && update.installedOn ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Previously Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!hasChanges &&
|
||||
<div>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasChanges &&
|
||||
<div className={styles.changes}>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!updatesError &&
|
||||
<div>
|
||||
Failed to fetch updates
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!generalSettingsError &&
|
||||
<div>
|
||||
Failed to update settings
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Updates.propTypes = {
|
||||
currentVersion: PropTypes.string.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
updatesError: PropTypes.object,
|
||||
generalSettingsError: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
isInstallingUpdate: PropTypes.bool.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
updateMechanism: PropTypes.string,
|
||||
updateMechanismMessage: PropTypes.string,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onInstallLatestPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Updates;
|
||||
303
frontend/src/System/Updates/Updates.tsx
Normal file
303
frontend/src/System/Updates/Updates.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { UpdateMechanism } from 'typings/Settings/General';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
||||
|
||||
function createUpdatesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.system.updates,
|
||||
(state: AppState) => state.settings.general,
|
||||
(updates, generalSettings) => {
|
||||
const { error: updatesError, items } = updates;
|
||||
|
||||
const isFetching = updates.isFetching || generalSettings.isFetching;
|
||||
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError: generalSettings.error,
|
||||
items,
|
||||
updateMechanism: generalSettings.item.updateMechanism,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function Updates() {
|
||||
const currentVersion = useSelector((state: AppState) => state.app.version);
|
||||
const { packageUpdateMechanismMessage } = useSelector(
|
||||
createSystemStatusSelector()
|
||||
);
|
||||
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
const isInstallingUpdate = useSelector(
|
||||
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
|
||||
);
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
updateMechanism,
|
||||
} = useSelector(createUpdatesSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
|
||||
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
|
||||
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
|
||||
external: translate('ExternalUpdater'),
|
||||
apt: translate('AptUpdater'),
|
||||
docker: translate('DockerUpdater'),
|
||||
};
|
||||
|
||||
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
|
||||
const majorVersion = parseInt(
|
||||
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
const latestVersion = items[0]?.version;
|
||||
const latestMajorVersion = parseInt(
|
||||
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
return {
|
||||
isMajorUpdate: latestMajorVersion > majorVersion,
|
||||
hasUpdateToInstall: items.some(
|
||||
(update) => update.installable && update.latest
|
||||
),
|
||||
};
|
||||
}, [currentVersion, items]);
|
||||
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const handleInstallLatestPress = useCallback(() => {
|
||||
if (isMajorUpdate) {
|
||||
setIsMajorUpdateModalOpen(true);
|
||||
} else {
|
||||
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
|
||||
}
|
||||
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleInstallLatestMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.APPLICATION_UPDATE,
|
||||
installMajorUpdate: true,
|
||||
})
|
||||
);
|
||||
}, [setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleCancelMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
}, [setIsMajorUpdateModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUpdates());
|
||||
dispatch(fetchGeneralSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{isPopulated || hasError ? null : <LoadingIndicator />}
|
||||
|
||||
{noUpdates ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
|
||||
) : null}
|
||||
|
||||
{hasUpdateToInstall ? (
|
||||
<div className={styles.messageContainer}>
|
||||
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={handleInstallLatestPress}
|
||||
>
|
||||
{translate('InstallLatest')}
|
||||
</SpinnerButton>
|
||||
) : (
|
||||
<>
|
||||
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix}{' '}
|
||||
<InlineMarkdown
|
||||
data={
|
||||
packageUpdateMechanismMessage ||
|
||||
externalUpdaterMessages[updateMechanism] ||
|
||||
externalUpdaterMessages.external
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isFetching ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{noUpdateToInstall && (
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
<div className={styles.message}>{translate('OnLatestVersion')}</div>
|
||||
|
||||
{isFetching && (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasUpdates && (
|
||||
<div>
|
||||
{items.map((update) => {
|
||||
return (
|
||||
<div key={update.version} className={styles.update}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(
|
||||
update.releaseDate,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{update.branch === 'master' ? null : (
|
||||
<Label className={styles.label}>{update.branch}</Label>
|
||||
)}
|
||||
|
||||
{update.version === currentVersion ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('CurrentlyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
|
||||
{update.version !== currentVersion && update.installedOn ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('PreviouslyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{update.changes ? (
|
||||
<div>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>{translate('MaintenanceRelease')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatesError ? (
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('FailedToFetchUpdates')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{generalSettingsError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('FailedToFetchSettings')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMajorUpdateModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('InstallMajorVersionUpdate')}
|
||||
message={
|
||||
<div>
|
||||
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('InstallMajorVersionUpdateMessageLink', {
|
||||
domain: 'readarr.com',
|
||||
url: 'https://readarr.com/#downloads',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Install')}
|
||||
onConfirm={handleInstallLatestMajorVersionPress}
|
||||
onCancel={handleCancelMajorVersionPress}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Updates;
|
||||
@@ -8,6 +8,7 @@ window.console.debug = window.console.debug || function() {};
|
||||
window.console.warn = window.console.warn || function() {};
|
||||
window.console.assert = window.console.assert || function() {};
|
||||
|
||||
// TODO: Remove in v5, well suppoprted in browsers
|
||||
if (!String.prototype.startsWith) {
|
||||
Object.defineProperty(String.prototype, 'startsWith', {
|
||||
enumerable: false,
|
||||
@@ -20,6 +21,7 @@ if (!String.prototype.startsWith) {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Remove in v5, well suppoprted in browsers
|
||||
if (!String.prototype.endsWith) {
|
||||
Object.defineProperty(String.prototype, 'endsWith', {
|
||||
enumerable: false,
|
||||
@@ -34,8 +36,14 @@ if (!String.prototype.endsWith) {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Remove in v5, use `includes` instead
|
||||
if (!('contains' in String.prototype)) {
|
||||
String.prototype.contains = function(str, startIndex) {
|
||||
return String.prototype.indexOf.call(this, str, startIndex) !== -1;
|
||||
};
|
||||
}
|
||||
|
||||
// For Firefox ESR 115 support
|
||||
if (!Object.groupBy) {
|
||||
import('core-js/actual/object/group-by');
|
||||
}
|
||||
|
||||
45
frontend/src/typings/Settings/General.ts
Normal file
45
frontend/src/typings/Settings/General.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export type UpdateMechanism =
|
||||
| 'builtIn'
|
||||
| 'script'
|
||||
| 'external'
|
||||
| 'apt'
|
||||
| 'docker';
|
||||
|
||||
export default interface General {
|
||||
bindAddress: string;
|
||||
port: number;
|
||||
sslPort: number;
|
||||
enableSsl: boolean;
|
||||
launchBrowser: boolean;
|
||||
authenticationMethod: string;
|
||||
authenticationRequired: string;
|
||||
analyticsEnabled: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
logLevel: string;
|
||||
consoleLogLevel: string;
|
||||
branch: string;
|
||||
apiKey: string;
|
||||
sslCertPath: string;
|
||||
sslCertPassword: string;
|
||||
urlBase: string;
|
||||
instanceName: string;
|
||||
applicationUrl: string;
|
||||
updateAutomatically: boolean;
|
||||
updateMechanism: UpdateMechanism;
|
||||
updateScriptPath: string;
|
||||
proxyEnabled: boolean;
|
||||
proxyType: string;
|
||||
proxyHostname: string;
|
||||
proxyPort: number;
|
||||
proxyUsername: string;
|
||||
proxyPassword: string;
|
||||
proxyBypassFilter: string;
|
||||
proxyBypassLocalAddresses: boolean;
|
||||
certificateValidation: string;
|
||||
backupFolder: string;
|
||||
backupInterval: number;
|
||||
backupRetention: number;
|
||||
id: number;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface UiSettings {
|
||||
export default interface UiSettings {
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
32
frontend/src/typings/SystemStatus.ts
Normal file
32
frontend/src/typings/SystemStatus.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
interface SystemStatus {
|
||||
appData: string;
|
||||
appName: string;
|
||||
authentication: string;
|
||||
branch: string;
|
||||
buildTime: string;
|
||||
instanceName: string;
|
||||
isAdmin: boolean;
|
||||
isDebug: boolean;
|
||||
isDocker: boolean;
|
||||
isLinux: boolean;
|
||||
isNetCore: boolean;
|
||||
isOsx: boolean;
|
||||
isProduction: boolean;
|
||||
isUserInteractive: boolean;
|
||||
isWindows: boolean;
|
||||
migrationVersion: number;
|
||||
mode: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
packageUpdateMechanism: string;
|
||||
packageUpdateMechanismMessage: string;
|
||||
runtimeName: string;
|
||||
runtimeVersion: string;
|
||||
sqliteVersion: string;
|
||||
startTime: string;
|
||||
startupPath: string;
|
||||
urlBase: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export default SystemStatus;
|
||||
20
frontend/src/typings/Update.ts
Normal file
20
frontend/src/typings/Update.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface Changes {
|
||||
new: string[];
|
||||
fixed: string[];
|
||||
}
|
||||
|
||||
interface Update {
|
||||
version: string;
|
||||
branch: string;
|
||||
releaseDate: string;
|
||||
fileName: string;
|
||||
url: string;
|
||||
installed: boolean;
|
||||
installedOn: string;
|
||||
installable: boolean;
|
||||
latest: boolean;
|
||||
changes: Changes | null;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export default Update;
|
||||
94
package.json
94
package.json
@@ -25,34 +25,33 @@
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@fortawesome/fontawesome-free": "6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.19.31",
|
||||
"@sentry/browser": "7.119.1",
|
||||
"@sentry/integrations": "7.119.1",
|
||||
"@types/node": "20.16.11",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"ansi-colors": "4.1.3",
|
||||
"classnames": "2.3.2",
|
||||
"classnames": "2.5.1",
|
||||
"clipboard": "2.0.11",
|
||||
"connected-react-router": "6.9.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.0.7",
|
||||
"filesize": "10.1.6",
|
||||
"fuse.js": "6.6.2",
|
||||
"history": "4.10.1",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.7.0",
|
||||
"jquery": "3.7.1",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.4",
|
||||
"moment": "2.30.1",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.1",
|
||||
"qs": "6.13.0",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
@@ -64,7 +63,7 @@
|
||||
"react-dnd-touch-backend": "14.1.1",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.5.2",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "2.5.2",
|
||||
@@ -73,74 +72,71 @@
|
||||
"react-redux": "7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-slider": "1.3.1",
|
||||
"react-tabs": "3.2.2",
|
||||
"react-text-truncate": "0.18.0",
|
||||
"react-slider": "1.3.3",
|
||||
"react-tabs": "4.3.0",
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
"redux": "4.1.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/core": "7.25.8",
|
||||
"@babel/eslint-parser": "7.25.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.8",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@babel/preset-react": "7.25.7",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-lazyload": "3.2.0",
|
||||
"@types/redux-actions": "2.6.2",
|
||||
"@types/react-lazyload": "3.2.3",
|
||||
"@types/redux-actions": "2.6.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.3",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.37.0",
|
||||
"core-js": "3.38.1",
|
||||
"css-loader": "6.8.1",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-json": "3.1.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"eslint-plugin-react": "7.37.1",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"html-webpack-plugin": "5.6.0",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"postcss": "8.4.38",
|
||||
"mini-css-extract-plugin": "2.9.1",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
"postcss-nested": "6.0.1",
|
||||
"postcss-nested": "6.2.0",
|
||||
"postcss-simple-vars": "7.0.1",
|
||||
"postcss-url": "10.1.3",
|
||||
"prettier": "2.8.8",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "4.4.1",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.3",
|
||||
"rimraf": "6.0.1",
|
||||
"style-loader": "3.3.4",
|
||||
"stylelint": "15.10.3",
|
||||
"stylelint-order": "6.0.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-loader": "9.4.4",
|
||||
"stylelint-order": "6.0.4",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-loader": "9.5.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.88.2",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-livereload-plugin": "3.0.2",
|
||||
"worker-loader": "3.0.8"
|
||||
|
||||
@@ -4,26 +4,27 @@
|
||||
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
||||
<PackageVersion Include="Dapper" Version="2.0.151" />
|
||||
<PackageVersion Include="Diacritical.Net" Version="1.0.4" />
|
||||
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageVersion Include="Polly" Version="8.3.1" />
|
||||
<PackageVersion Include="Polly" Version="8.5.0" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
<PackageVersion Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.2.0" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
||||
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
||||
<PackageVersion Include="Mailkit" Version="3.6.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.29" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
||||
<PackageVersion Include="Moq" Version="4.17.2" />
|
||||
@@ -33,20 +34,21 @@
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
|
||||
<PackageVersion Include="NLog" Version="5.1.4" />
|
||||
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageVersion Include="Npgsql" Version="7.0.6" />
|
||||
<PackageVersion Include="Npgsql" Version="7.0.9" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageVersion Include="NUnit" Version="3.14.0" />
|
||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.32" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.65" />
|
||||
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
||||
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
||||
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
|
||||
<PackageVersion Include="Sentry" Version="3.31.0" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
||||
<PackageVersion Include="System.Buffers" Version="4.5.1" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
@@ -60,7 +62,7 @@
|
||||
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="6.0.9" />
|
||||
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
|
||||
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||
_runner.KillAll();
|
||||
_runner.Start();
|
||||
_runner.Start(true);
|
||||
|
||||
driver.Url = "http://localhost:8787";
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -21,9 +21,28 @@ namespace NzbDrone.Common.Test.ExtensionTests
|
||||
[TestCase("1.2.3.4")]
|
||||
[TestCase("172.55.0.1")]
|
||||
[TestCase("192.55.0.1")]
|
||||
[TestCase("100.64.0.1")]
|
||||
[TestCase("100.127.255.254")]
|
||||
public void should_return_false_for_public_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("100.64.0.1")]
|
||||
[TestCase("100.127.255.254")]
|
||||
[TestCase("100.100.100.100")]
|
||||
public void should_return_true_for_cgnat_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("1.2.3.4")]
|
||||
[TestCase("192.168.5.1")]
|
||||
[TestCase("100.63.255.255")]
|
||||
[TestCase("100.128.0.0")]
|
||||
public void should_return_false_for_non_cgnat_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
||||
|
||||
// Telegram
|
||||
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/sendmessage: chat_id=123456&parse_mode=HTML&text=<text>")]
|
||||
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/")]
|
||||
|
||||
public void should_clean_message(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
@@ -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;
|
||||
@@ -33,6 +34,11 @@ namespace NzbDrone.Common.Test
|
||||
|
||||
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();
|
||||
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
||||
|
||||
@@ -39,18 +39,24 @@ namespace NzbDrone.Common.Extensions
|
||||
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
||||
{
|
||||
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||
var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||
|
||||
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||
var isClassA = ipv4Bytes[0] == 10;
|
||||
|
||||
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||
var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||
|
||||
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||
var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||
|
||||
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||
return isLinkLocal || isClassA || isClassC || isClassB;
|
||||
}
|
||||
|
||||
public static bool IsCgnatIpAddress(this IPAddress ipAddress)
|
||||
{
|
||||
var bytes = ipAddress.GetAddressBytes();
|
||||
return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,10 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new (@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Discord
|
||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Telegram
|
||||
new (@"api.telegram.org/bot(?<id>[\d]+):(?<secret>[\w-]+)/", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
8
src/NzbDrone.Common/Options/AppOptions.cs
Normal file
8
src/NzbDrone.Common/Options/AppOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class AppOptions
|
||||
{
|
||||
public string InstanceName { get; set; }
|
||||
public string Theme { get; set; }
|
||||
public bool? LaunchBrowser { get; set; }
|
||||
}
|
||||
10
src/NzbDrone.Common/Options/AuthOptions.cs
Normal file
10
src/NzbDrone.Common/Options/AuthOptions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
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; }
|
||||
public bool? TrustCgnatIpAddresses { get; set; }
|
||||
}
|
||||
14
src/NzbDrone.Common/Options/LogOptions.cs
Normal file
14
src/NzbDrone.Common/Options/LogOptions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
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; }
|
||||
}
|
||||
12
src/NzbDrone.Common/Options/ServerOptions.cs
Normal file
12
src/NzbDrone.Common/Options/ServerOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class ServerOptions
|
||||
{
|
||||
public string UrlBase { get; set; }
|
||||
public string BindAddress { get; set; }
|
||||
public int? Port { get; set; }
|
||||
public bool? EnableSsl { get; set; }
|
||||
public int? SslPort { get; set; }
|
||||
public string SslCertPath { get; set; }
|
||||
public string SslCertPassword { get; set; }
|
||||
}
|
||||
9
src/NzbDrone.Common/Options/UpdateOptions.cs
Normal file
9
src/NzbDrone.Common/Options/UpdateOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class UpdateOptions
|
||||
{
|
||||
public string Mechanism { get; set; }
|
||||
public bool? Automatically { get; set; }
|
||||
public string ScriptPath { get; set; }
|
||||
public string Branch { get; set; }
|
||||
}
|
||||
@@ -313,7 +313,7 @@ namespace NzbDrone.Common.Processes
|
||||
processInfo = new ProcessInfo();
|
||||
processInfo.Id = process.Id;
|
||||
processInfo.Name = process.ProcessName;
|
||||
processInfo.StartPath = process.MainModule.FileName;
|
||||
processInfo.StartPath = process.MainModule?.FileName;
|
||||
|
||||
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
||||
{
|
||||
|
||||
@@ -200,17 +200,9 @@ namespace NzbDrone.Core.Test.Download
|
||||
var seriesTags = new HashSet<int> { 2 };
|
||||
var clientTags = new HashSet<int> { 1 };
|
||||
|
||||
WithTorrentClient(0, clientTags);
|
||||
WithTorrentClient(0, clientTags);
|
||||
WithTorrentClient(0, clientTags);
|
||||
WithTorrentClient(0, clientTags);
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
|
||||
Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags).Should().BeNull();
|
||||
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -312,11 +312,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
||||
[Test]
|
||||
public void should_return_status_with_outputdirs()
|
||||
{
|
||||
var configItems = new Dictionary<string, object>();
|
||||
|
||||
configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic());
|
||||
configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||
configItems.Add("move_completed", true);
|
||||
var configItems = new Dictionary<string, object>
|
||||
{
|
||||
{ "download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic() },
|
||||
{ "move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic() },
|
||||
{ "move_completed", true }
|
||||
};
|
||||
|
||||
Mocker.GetMock<IDelugeProxy>()
|
||||
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
|
||||
@@ -328,5 +329,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
||||
result.OutputRootFolders.Should().NotBeNull();
|
||||
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_status_with_outputdirs_for_directories_in_settings()
|
||||
{
|
||||
Subject.Definition.Settings.As<DelugeSettings>().DownloadDirectory = @"D:\Downloads\Downloading\deluge".AsOsAgnostic();
|
||||
Subject.Definition.Settings.As<DelugeSettings>().CompletedDirectory = @"D:\Downloads\Finished\deluge".AsOsAgnostic();
|
||||
|
||||
var result = Subject.GetStatus();
|
||||
|
||||
result.IsLocalhost.Should().BeTrue();
|
||||
result.OutputRootFolders.Should().NotBeNull();
|
||||
result.OutputRootFolders.First().Should().Be(@"D:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
VerifyWarning(item);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void paused_item_should_have_required_properties()
|
||||
[TestCase("pausedDL")]
|
||||
[TestCase("stoppedDL")]
|
||||
public void paused_item_should_have_required_properties(string state)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
@@ -188,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = "pausedDL",
|
||||
State = state,
|
||||
Label = "",
|
||||
SavePath = ""
|
||||
};
|
||||
@@ -200,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
[TestCase("queuedUP")]
|
||||
[TestCase("uploading")]
|
||||
[TestCase("stalledUP")]
|
||||
@@ -397,8 +399,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void api_261_should_use_content_path()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void api_261_should_use_content_path(string state)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
@@ -407,7 +410,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = "pausedUP",
|
||||
State = state,
|
||||
Label = "",
|
||||
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
||||
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
||||
@@ -557,6 +560,34 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\QBittorrent".AsOsAgnostic());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_correct_category_output_path()
|
||||
{
|
||||
var config = new QBittorrentPreferences
|
||||
{
|
||||
SavePath = @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic()
|
||||
};
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(v => v.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(config);
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(v => v.GetApiVersion(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new Version(2, 0));
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetLabels(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new Dictionary<string, QBittorrentLabel>
|
||||
{ { "music", new QBittorrentLabel { Name = "music", SavePath = "//server/store/downloads" } } });
|
||||
|
||||
var result = Subject.GetStatus();
|
||||
|
||||
result.IsLocalhost.Should().BeTrue();
|
||||
result.OutputRootFolders.Should().NotBeNull();
|
||||
result.OutputRootFolders.First().Should().Be(@"\\server\store\downloads");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Download_should_handle_http_redirect_to_magnet()
|
||||
{
|
||||
@@ -656,44 +687,48 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(0.2f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
|
||||
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
@@ -711,33 +746,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 40);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
@@ -755,66 +793,72 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_fetch_details_twice()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_fetch_details_twice(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 30);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
@@ -826,8 +870,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_category_from_the_category_if_set()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_get_category_from_the_category_if_set(string state)
|
||||
{
|
||||
const string category = "music-readarr";
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
@@ -839,7 +884,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "pausedUP",
|
||||
State = state,
|
||||
Category = category,
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
@@ -851,8 +896,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
item.Category.Should().Be(category);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_category_from_the_label_if_the_category_is_not_available()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
|
||||
{
|
||||
const string category = "music-readarr";
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
@@ -864,7 +910,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "pausedUP",
|
||||
State = state,
|
||||
Label = category,
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
|
||||
@@ -478,6 +478,37 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("all", 0)]
|
||||
[TestCase("days-archive", 15)]
|
||||
[TestCase("days-delete", 15)]
|
||||
public void should_set_history_removes_completed_downloads_false_for_separate_properties(string option, int number)
|
||||
{
|
||||
_config.Misc.history_retention_option = option;
|
||||
_config.Misc.history_retention_number = number;
|
||||
|
||||
var downloadClientInfo = Subject.GetStatus();
|
||||
|
||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("number-archive", 10)]
|
||||
[TestCase("number-delete", 10)]
|
||||
[TestCase("number-archive", 0)]
|
||||
[TestCase("number-delete", 0)]
|
||||
[TestCase("days-archive", 3)]
|
||||
[TestCase("days-delete", 3)]
|
||||
[TestCase("all-archive", 0)]
|
||||
[TestCase("all-delete", 0)]
|
||||
public void should_set_history_removes_completed_downloads_true_for_separate_properties(string option, int number)
|
||||
{
|
||||
_config.Misc.history_retention_option = option;
|
||||
_config.Misc.history_retention_number = number;
|
||||
|
||||
var downloadClientInfo = Subject.GetStatus();
|
||||
|
||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")]
|
||||
[TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")]
|
||||
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
|
||||
|
||||
@@ -49,10 +49,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void magnet_download_should_not_return_the_item()
|
||||
public void magnet_download_should_be_returned_as_queued()
|
||||
{
|
||||
PrepareClientToReturnMagnetItem();
|
||||
Subject.GetItems().Count().Should().Be(0);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
|
||||
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -60,7 +60,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
|
||||
public void magnet_download_should_not_return_the_item()
|
||||
{
|
||||
PrepareClientToReturnMagnetItem();
|
||||
Subject.GetItems().Count().Should().Be(0);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
|
||||
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Core.HealthCheck.Checks;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
{
|
||||
@@ -21,28 +22,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
.Returns("Some Warning Message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_error_when_app_folder_is_write_protected()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
Mocker.GetMock<IAppFolderInfo>()
|
||||
.Setup(s => s.StartUpFolder)
|
||||
.Returns(@"C:\NzbDrone");
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(c => c.FolderWritable(It.IsAny<string>()))
|
||||
.Returns(false);
|
||||
|
||||
Subject.Check().ShouldBeError();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||
{
|
||||
PosixOnly();
|
||||
|
||||
const string startupFolder = @"/opt/nzbdrone";
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -62,10 +45,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||
{
|
||||
PosixOnly();
|
||||
|
||||
const string startupFolder = @"/opt/nzbdrone";
|
||||
const string uiFolder = @"/opt/nzbdrone/UI";
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic();
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -89,7 +70,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled()
|
||||
{
|
||||
PosixOnly();
|
||||
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>()
|
||||
.Setup(s => s.UpdateAutomatically)
|
||||
@@ -101,7 +82,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
|
||||
Mocker.GetMock<IAppFolderInfo>()
|
||||
.Setup(s => s.StartUpFolder)
|
||||
.Returns(@"/opt/nzbdrone");
|
||||
.Returns(startupFolder);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Verify(c => c.FolderWritable(It.IsAny<string>()), Times.Never());
|
||||
|
||||
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2024-05-15 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-05-15 00:00:00Z")]
|
||||
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
private MetadataProfile _metadataProfile;
|
||||
|
||||
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2024-05-15 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2025-05-15 00:00:00Z")]
|
||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
[SetUp]
|
||||
|
||||
@@ -38,9 +38,9 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
ExceptionVerification.IgnoreWarns();
|
||||
}
|
||||
|
||||
[TestCase("Harry Potter and the sorcerer's stone a detailed summary", 61800696)]
|
||||
[TestCase("Harry Potter and the sorcerer's stone a detailed summary", 72245296)]
|
||||
[TestCase("B0192CTMYG", 61209488)]
|
||||
[TestCase("9780439554930", 48517161)]
|
||||
[TestCase("9780439554930", 3)]
|
||||
public void successful_book_search(string title, int expected)
|
||||
{
|
||||
var result = Subject.Search(title);
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace NzbDrone.Core.Test.QueueTests
|
||||
|
||||
_trackedDownloads = Builder<TrackedDownload>.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(v => v.IsTrackable = true)
|
||||
.With(v => v.DownloadItem = downloadItem)
|
||||
.With(v => v.RemoteBook = remoteBook)
|
||||
.Build()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationRequiredType
|
||||
{
|
||||
Enabled = 0,
|
||||
DisabledForLocalAddresses = 1
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
namespace NzbDrone.Core.Authentication
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationType
|
||||
{
|
||||
None = 0,
|
||||
Basic = 1,
|
||||
Forms = 2
|
||||
Forms = 2,
|
||||
External = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,18 @@ namespace NzbDrone.Core.Books
|
||||
public class BookCutoffService : IBookCutoffService
|
||||
{
|
||||
private readonly IBookRepository _bookRepository;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IQualityProfileService _qualityProfileService;
|
||||
|
||||
public BookCutoffService(IBookRepository bookRepository, IProfileService profileService)
|
||||
public BookCutoffService(IBookRepository bookRepository, IQualityProfileService qualityProfileService)
|
||||
{
|
||||
_bookRepository = bookRepository;
|
||||
_profileService = profileService;
|
||||
_qualityProfileService = qualityProfileService;
|
||||
}
|
||||
|
||||
public PagingSpec<Book> BooksWhereCutoffUnmet(PagingSpec<Book> pagingSpec)
|
||||
{
|
||||
var qualitiesBelowCutoff = new List<QualitiesBelowCutoff>();
|
||||
var profiles = _profileService.All();
|
||||
var profiles = _qualityProfileService.All();
|
||||
|
||||
//Get all items less than the cutoff
|
||||
foreach (var profile in profiles)
|
||||
|
||||
@@ -9,6 +9,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;
|
||||
@@ -32,6 +33,7 @@ namespace NzbDrone.Core.Configuration
|
||||
bool EnableSsl { get; }
|
||||
bool LaunchBrowser { get; }
|
||||
AuthenticationType AuthenticationMethod { get; }
|
||||
AuthenticationRequiredType AuthenticationRequired { get; }
|
||||
bool AnalyticsEnabled { get; }
|
||||
string LogLevel { get; }
|
||||
string ConsoleLogLevel { get; }
|
||||
@@ -51,6 +53,7 @@ namespace NzbDrone.Core.Configuration
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string SyslogLevel { get; }
|
||||
string Theme { get; }
|
||||
string PostgresHost { get; }
|
||||
int PostgresPort { get; }
|
||||
string PostgresUser { get; }
|
||||
@@ -58,7 +61,7 @@ namespace NzbDrone.Core.Configuration
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
string PostgresCacheDb { get; }
|
||||
string Theme { get; }
|
||||
bool TrustCgnatIpAddresses { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -69,6 +72,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;
|
||||
|
||||
@@ -78,13 +86,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()
|
||||
@@ -140,7 +158,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;
|
||||
@@ -150,19 +168,19 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public int Port => GetValueInt("Port", 8787);
|
||||
public int Port => _serverOptions.Port ?? GetValueInt("Port", 8787);
|
||||
|
||||
public int SslPort => GetValueInt("SslPort", 6868);
|
||||
public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", 6868);
|
||||
|
||||
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())
|
||||
{
|
||||
@@ -178,7 +196,7 @@ namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
var enabled = GetValueBoolean("AuthenticationEnabled", false, false);
|
||||
var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false);
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
@@ -186,17 +204,24 @@ 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 bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||
public AuthenticationRequiredType AuthenticationRequired =>
|
||||
Enum.TryParse<AuthenticationRequiredType>(_authOptions.Required, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||
|
||||
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", "develop").ToLowerInvariant();
|
||||
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "develop").ToLowerInvariant();
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info");
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? 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);
|
||||
@@ -206,18 +231,18 @@ namespace NzbDrone.Core.Configuration
|
||||
public string PostgresCacheDb => _postgresOptions?.CacheDb ?? GetValue("PostgresCacheDb", "readarr-cache", 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 string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", 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())
|
||||
{
|
||||
@@ -229,19 +254,22 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
|
||||
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
|
||||
public string InstanceName => GetValue("InstanceName", BuildInfo.AppName);
|
||||
public string InstanceName => _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
|
||||
|
||||
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
|
||||
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false);
|
||||
|
||||
public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
|
||||
public UpdateMechanism UpdateMechanism =>
|
||||
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
|
||||
|
||||
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
|
||||
public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false);
|
||||
|
||||
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
|
||||
public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false);
|
||||
|
||||
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
|
||||
public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false);
|
||||
|
||||
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, false).ToLowerInvariant();
|
||||
public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
|
||||
|
||||
public int GetValueInt(string key, int defaultValue, bool persist = true)
|
||||
{
|
||||
@@ -330,7 +358,7 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
|
||||
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
|
||||
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||
if (EnableSsl && (GetValue("SslCertHash", string.Empty, false).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||
{
|
||||
SetValue("EnableSsl", false);
|
||||
}
|
||||
@@ -377,13 +405,21 @@ namespace NzbDrone.Core.Configuration
|
||||
throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Readarr will recreate it.");
|
||||
}
|
||||
|
||||
return XDocument.Parse(_diskProvider.ReadAllText(_configFile));
|
||||
var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile));
|
||||
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList();
|
||||
|
||||
if (config.Count != 1)
|
||||
{
|
||||
throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Readarr will recreate it.");
|
||||
}
|
||||
|
||||
return xDoc;
|
||||
}
|
||||
|
||||
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
|
||||
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
|
||||
var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
|
||||
newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
|
||||
|
||||
return xDoc;
|
||||
return newXDoc;
|
||||
}
|
||||
}
|
||||
catch (XmlException ex)
|
||||
@@ -427,5 +463,7 @@ namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
SetValue("ApiKey", GenerateApiKey());
|
||||
}
|
||||
|
||||
public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +404,12 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
||||
|
||||
public bool TrustCgnatIpAddresses
|
||||
{
|
||||
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
|
||||
set { SetValue("TrustCgnatIpAddresses", value); }
|
||||
}
|
||||
|
||||
private string GetValue(string key)
|
||||
{
|
||||
return GetValue(key, string.Empty);
|
||||
|
||||
@@ -219,7 +219,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
|
||||
protected virtual IList<TableDefinition> ReadTables()
|
||||
{
|
||||
const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;";
|
||||
const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name;";
|
||||
var dtTable = Read(sqlCommand).Tables[0];
|
||||
|
||||
var tableDefinitionList = new List<TableDefinition>();
|
||||
|
||||
@@ -122,14 +122,23 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
}
|
||||
|
||||
var items = new List<DownloadClientItem>();
|
||||
var ignoredCount = 0;
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
if (torrent.Hash == null)
|
||||
// Silently ignore torrents with no hash
|
||||
if (torrent.Hash.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore torrents without a name, but track to log a single warning for all invalid torrents.
|
||||
if (torrent.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
ignoredCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem();
|
||||
item.DownloadId = torrent.Hash.ToUpper();
|
||||
item.Title = torrent.Name;
|
||||
@@ -187,6 +196,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
if (ignoredCount > 0)
|
||||
{
|
||||
_logger.Warn("{0} torrent(s) were ignored becuase they did not have a title, check Deluge and remove any invalid torrents");
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -199,9 +213,18 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var label = _proxy.GetLabelOptions(Settings);
|
||||
|
||||
OsPath destDir;
|
||||
|
||||
if (label != null && label.ApplyMoveCompleted && label.MoveCompleted)
|
||||
if (Settings.CompletedDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
destDir = new OsPath(Settings.CompletedDirectory);
|
||||
}
|
||||
else if (Settings.DownloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
destDir = new OsPath(Settings.DownloadDirectory);
|
||||
}
|
||||
else if (label is { ApplyMoveCompleted: true, MoveCompleted: true })
|
||||
{
|
||||
// if label exists and a label completed path exists and is enabled use it instead of global
|
||||
destDir = new OsPath(label.MoveCompletedPath);
|
||||
@@ -217,7 +240,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
|
||||
var status = new DownloadClientInfo
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost"
|
||||
IsLocalhost = Settings.Host is "127.0.0.1" or "localhost"
|
||||
};
|
||||
|
||||
if (!destDir.IsEmpty)
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
|
||||
// Avoid removing torrents that haven't reached the global max ratio.
|
||||
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
||||
item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config);
|
||||
item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config);
|
||||
|
||||
switch (torrent.State)
|
||||
{
|
||||
@@ -248,7 +248,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
item.Message = "qBittorrent is reporting an error";
|
||||
break;
|
||||
|
||||
case "pausedDL": // torrent is paused and has NOT finished downloading
|
||||
case "stoppedDL": // torrent is stopped and has NOT finished downloading
|
||||
case "pausedDL": // torrent is paused and has NOT finished downloading (qBittorrent < 5)
|
||||
item.Status = DownloadItemStatus.Paused;
|
||||
break;
|
||||
|
||||
@@ -259,7 +260,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
break;
|
||||
|
||||
case "pausedUP": // torrent is paused and has finished downloading
|
||||
case "pausedUP": // torrent is paused and has finished downloading (qBittorent < 5)
|
||||
case "stoppedUP": // torrent is stopped and has finished downloading
|
||||
case "uploading": // torrent is being seeded and data is being transferred
|
||||
case "stalledUP": // torrent is being seeded, but no connection were made
|
||||
case "queuedUP": // queuing is enabled and torrent is queued for upload
|
||||
@@ -279,6 +281,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
break;
|
||||
|
||||
case "metaDL": // torrent magnet is being downloaded
|
||||
case "forcedMetaDL": // torrent metadata is being forcibly downloaded
|
||||
if (config.DhtEnabled)
|
||||
{
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
@@ -293,7 +296,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
break;
|
||||
|
||||
case "forcedDL": // torrent is being downloaded, and was forced started
|
||||
case "forcedMetaDL": // torrent metadata is being forcibly downloaded
|
||||
case "moving": // torrent is being moved from a folder
|
||||
case "downloading": // torrent is being downloaded and data is being transferred
|
||||
item.Status = DownloadItemStatus.Downloading;
|
||||
@@ -375,7 +377,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
if (Proxy.GetLabels(Settings).TryGetValue(Settings.MusicCategory, out var label) && label.SavePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var labelDir = new OsPath(label.SavePath);
|
||||
var savePath = label.SavePath;
|
||||
|
||||
if (savePath.StartsWith("//"))
|
||||
{
|
||||
_logger.Trace("Replacing double forward slashes in path '{0}'. If this is not meant to be a Windows UNC path fix the 'Save Path' in qBittorrent's {1} category", savePath, Settings.MusicCategory);
|
||||
savePath = savePath.Replace('/', '\\');
|
||||
}
|
||||
|
||||
var labelDir = new OsPath(savePath);
|
||||
|
||||
if (labelDir.IsRooted)
|
||||
{
|
||||
|
||||
@@ -26,8 +26,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings);
|
||||
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
|
||||
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
|
||||
void PauseTorrent(string hash, QBittorrentSettings settings);
|
||||
void ResumeTorrent(string hash, QBittorrentSettings settings);
|
||||
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
|
||||
.Post()
|
||||
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -266,22 +266,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
||||
|
||||
@@ -246,14 +246,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
request.AddFormParameter("category", settings.MusicCategory);
|
||||
}
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
|
||||
// Avoid extraneous API version check if initial state is ForceStart
|
||||
if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused";
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
|
||||
{
|
||||
request.AddFormParameter(stoppedParameterName, false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter(stoppedParameterName, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.SequentialOrder)
|
||||
@@ -291,7 +297,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -313,7 +319,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -322,22 +328,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
public enum QBittorrentState
|
||||
{
|
||||
[FieldOption(Label = "Started")]
|
||||
Start = 0,
|
||||
|
||||
[FieldOption(Label = "Force Started")]
|
||||
ForceStart = 1,
|
||||
Pause = 2
|
||||
|
||||
[FieldOption(Label = "Stopped")]
|
||||
Stop = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,20 +263,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
|
||||
}
|
||||
|
||||
if (config.Misc.history_retention.IsNullOrWhiteSpace())
|
||||
{
|
||||
status.RemovesCompletedDownloads = false;
|
||||
}
|
||||
else if (config.Misc.history_retention.EndsWith("d"))
|
||||
{
|
||||
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
|
||||
out var daysRetention);
|
||||
status.RemovesCompletedDownloads = daysRetention < 14;
|
||||
}
|
||||
else
|
||||
{
|
||||
status.RemovesCompletedDownloads = config.Misc.history_retention != "0";
|
||||
}
|
||||
status.RemovesCompletedDownloads = RemovesCompletedDownloads(config);
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -518,6 +505,43 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
return categories.Contains(category);
|
||||
}
|
||||
|
||||
private bool RemovesCompletedDownloads(SabnzbdConfig config)
|
||||
{
|
||||
var retention = config.Misc.history_retention;
|
||||
var option = config.Misc.history_retention_option;
|
||||
var number = config.Misc.history_retention_number;
|
||||
|
||||
switch (option)
|
||||
{
|
||||
case "all":
|
||||
return false;
|
||||
case "number-archive":
|
||||
case "number-delete":
|
||||
return true;
|
||||
case "days-archive":
|
||||
case "days-delete":
|
||||
return number < 14;
|
||||
case "all-archive":
|
||||
case "all-delete":
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Remove these checks once support for SABnzbd < 4.3 is removed
|
||||
if (retention.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (retention.EndsWith("d"))
|
||||
{
|
||||
int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1),
|
||||
out var daysRetention);
|
||||
return daysRetention < 14;
|
||||
}
|
||||
|
||||
return retention != "0";
|
||||
}
|
||||
|
||||
private bool ValidatePath(DownloadClientItem downloadClientItem)
|
||||
{
|
||||
var downloadItemOutputPath = downloadClientItem.OutputPath;
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||
public bool enable_date_sorting { get; set; }
|
||||
public bool pre_check { get; set; }
|
||||
public string history_retention { get; set; }
|
||||
public string history_retention_option { get; set; }
|
||||
public int history_retention_number { get; set; }
|
||||
}
|
||||
|
||||
public class SabnzbdCategory
|
||||
|
||||
@@ -41,12 +41,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
// If totalsize == 0 the torrent is a magnet downloading metadata
|
||||
if (torrent.TotalSize == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputPath = new OsPath(torrent.DownloadDir);
|
||||
|
||||
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
@@ -97,6 +91,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
item.Status = DownloadItemStatus.Warning;
|
||||
item.Message = torrent.ErrorString;
|
||||
}
|
||||
else if (torrent.TotalSize == 0)
|
||||
{
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
}
|
||||
else if (torrent.LeftUntilDone == 0 && (torrent.Status == TransmissionTorrentStatus.Stopped ||
|
||||
torrent.Status == TransmissionTorrentStatus.Seeding ||
|
||||
torrent.Status == TransmissionTorrentStatus.SeedingWait))
|
||||
|
||||
@@ -11,8 +11,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
public bool IsFinished { get; set; }
|
||||
public long Eta { get; set; }
|
||||
public TransmissionTorrentStatus Status { get; set; }
|
||||
public int SecondsDownloading { get; set; }
|
||||
public int SecondsSeeding { get; set; }
|
||||
public long SecondsDownloading { get; set; }
|
||||
public long SecondsSeeding { get; set; }
|
||||
public string ErrorString { get; set; }
|
||||
public long DownloadedEver { get; set; }
|
||||
public long UploadedEver { get; set; }
|
||||
|
||||
@@ -41,18 +41,23 @@ namespace NzbDrone.Core.Download
|
||||
var blockedProviders = new HashSet<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
|
||||
var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList();
|
||||
|
||||
if (tags != null)
|
||||
if (!availableProviders.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tags is { Count: > 0 })
|
||||
{
|
||||
var matchingTagsClients = availableProviders.Where(i => i.Definition.Tags.Intersect(tags).Any()).ToList();
|
||||
|
||||
availableProviders = matchingTagsClients.Count > 0 ?
|
||||
matchingTagsClients :
|
||||
availableProviders.Where(i => i.Definition.Tags.Empty()).ToList();
|
||||
}
|
||||
|
||||
if (!availableProviders.Any())
|
||||
{
|
||||
return null;
|
||||
if (!availableProviders.Any())
|
||||
{
|
||||
throw new DownloadClientUnavailableException("No download client was found without tags or a matching author tag. Please check your settings.");
|
||||
}
|
||||
}
|
||||
|
||||
if (indexerId > 0)
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
var startupFolder = _appFolderInfo.StartUpFolder;
|
||||
var uiFolder = Path.Combine(startupFolder, "UI");
|
||||
|
||||
if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) &&
|
||||
if (_configFileProvider.UpdateAutomatically &&
|
||||
_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn &&
|
||||
!_osInfo.IsDocker)
|
||||
{
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace NzbDrone.Core.ImportLists
|
||||
report.BookGoodreadsId = remoteBook.ForeignBookId;
|
||||
report.Book = remoteBook.Title;
|
||||
report.Author ??= remoteBook.AuthorMetadata.Value.Name;
|
||||
report.AuthorGoodreadsId ??= remoteBook.AuthorMetadata.Value.Name;
|
||||
report.AuthorGoodreadsId ??= remoteBook.AuthorMetadata.Value.ForeignAuthorId;
|
||||
}
|
||||
catch (BookNotFoundException)
|
||||
{
|
||||
|
||||
@@ -68,16 +68,17 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Torznab indexer?", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("{1} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
_logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -268,26 +268,26 @@ namespace NzbDrone.Core.Indexers
|
||||
protected virtual RssEnclosure[] GetEnclosures(XElement item)
|
||||
{
|
||||
var enclosures = item.Elements("enclosure")
|
||||
.Select(v =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new RssEnclosure
|
||||
{
|
||||
Url = v.Attribute("url")?.Value,
|
||||
Type = v.Attribute("type")?.Value,
|
||||
Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warn(e, "Failed to get enclosure for: {0}", item.Title());
|
||||
}
|
||||
.Select(v =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new RssEnclosure
|
||||
{
|
||||
Url = v.Attribute("url")?.Value,
|
||||
Type = v.Attribute("type")?.Value,
|
||||
Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to get enclosure for: {0}", item.Title());
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.Where(v => v != null)
|
||||
.ToArray();
|
||||
return null;
|
||||
})
|
||||
.Where(v => v != null)
|
||||
.ToArray();
|
||||
|
||||
return enclosures;
|
||||
}
|
||||
|
||||
@@ -48,10 +48,10 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
public class SeedCriteriaSettings
|
||||
{
|
||||
[FieldDefinition(0, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is download client's default. Ratio should be at least 1.0 and follow the indexers rules")]
|
||||
[FieldDefinition(0, Type = FieldType.Number, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")]
|
||||
public double? SeedRatio { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Textbox, Label = "Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)]
|
||||
[FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)]
|
||||
public int? SeedTime { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Textbox, Label = "Discography Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)]
|
||||
|
||||
@@ -59,16 +59,17 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(UsenetEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Newznab indexer?", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("{1} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
_logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -57,33 +57,36 @@ namespace NzbDrone.Core.Instrumentation
|
||||
{
|
||||
try
|
||||
{
|
||||
var log = new Log();
|
||||
log.Time = logEvent.TimeStamp;
|
||||
log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage);
|
||||
|
||||
log.Logger = logEvent.LoggerName;
|
||||
var log = new Log
|
||||
{
|
||||
Time = logEvent.TimeStamp,
|
||||
Logger = logEvent.LoggerName,
|
||||
Level = logEvent.Level.Name
|
||||
};
|
||||
|
||||
if (log.Logger.StartsWith("NzbDrone."))
|
||||
{
|
||||
log.Logger = log.Logger.Remove(0, 9);
|
||||
}
|
||||
|
||||
var message = logEvent.FormattedMessage;
|
||||
|
||||
if (logEvent.Exception != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(log.Message))
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
log.Message = logEvent.Exception.Message;
|
||||
message = logEvent.Exception.Message;
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Message += ": " + logEvent.Exception.Message;
|
||||
message += ": " + logEvent.Exception.Message;
|
||||
}
|
||||
|
||||
log.Exception = logEvent.Exception.ToString();
|
||||
log.Exception = CleanseLogMessage.Cleanse(logEvent.Exception.ToString());
|
||||
log.ExceptionType = logEvent.Exception.GetType().ToString();
|
||||
}
|
||||
|
||||
log.Level = logEvent.Level.Name;
|
||||
log.Message = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
var connectionInfo = _connectionStringFactory.LogDbConnection;
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"Year": "عام",
|
||||
"WeekColumnHeader": "رأس عمود الأسبوع",
|
||||
"Version": "الإصدار",
|
||||
"UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism": "يستخدم الفرع بواسطة آلية التحديث الخارجية",
|
||||
"UsingExternalUpdateMechanismBranchToUseToUpdateReadarr": "فرع لاستخدامه لتحديث Radarr",
|
||||
"BranchUpdateMechanism": "يستخدم الفرع بواسطة آلية التحديث الخارجية",
|
||||
"BranchUpdate": "فرع لاستخدامه لتحديث {appName}",
|
||||
"Username": "اسم المستخدم",
|
||||
"UsenetDelayHelpText": "تأخر بالدقائق للانتظار قبل الحصول على إصدار من Usenet",
|
||||
"UsenetDelay": "تأخير يوزنت",
|
||||
@@ -15,7 +15,7 @@
|
||||
"UpgradeAllowedHelpText": "إذا لن تتم ترقية الصفات المعوقين",
|
||||
"Updates": "التحديثات",
|
||||
"UpdateScriptPathHelpText": "المسار إلى برنامج نصي مخصص يأخذ حزمة تحديث مستخرجة ويتعامل مع ما تبقى من عملية التحديث",
|
||||
"UpdateMechanismHelpText": "استخدم المحدث أو البرنامج النصي المدمج في Radarr",
|
||||
"UpdateMechanismHelpText": "استخدم المحدث أو البرنامج النصي المدمج في {appName}",
|
||||
"UpdateAutomaticallyHelpText": "تنزيل التحديثات وتثبيتها تلقائيًا. ستظل قادرًا على التثبيت من النظام: التحديثات",
|
||||
"UpdateAll": "تحديث الجميع",
|
||||
"UnmonitoredHelpText": "قم بتضمين الأفلام غير الخاضعة للرقابة في موجز iCal",
|
||||
@@ -56,7 +56,7 @@
|
||||
"URLBase": "قاعدة URL",
|
||||
"UISettings": "إعدادات واجهة المستخدم",
|
||||
"UILanguageHelpTextWarning": "يلزم إعادة تحميل المتصفح",
|
||||
"UILanguageHelpText": "اللغة التي سيستخدمها Radarr لواجهة المستخدم",
|
||||
"UILanguageHelpText": "اللغة التي سيستخدمها {appName} لواجهة المستخدم",
|
||||
"UILanguage": "لغة واجهة المستخدم",
|
||||
"TotalFileSize": "إجمالي حجم الملف",
|
||||
"Torrents": "السيول",
|
||||
@@ -73,7 +73,7 @@
|
||||
"Tags": "العلامات",
|
||||
"TagIsNotUsedAndCanBeDeleted": "العلامة غير مستخدمة ويمكن حذفها",
|
||||
"SupportsSearchvalueWillBeUsedWhenInteractiveSearchIsUsed": "سيتم استخدامها عند استخدام البحث التفاعلي",
|
||||
"SupportsSearchvalueWillBeUsedWhenAutomaticSearchesArePerformedViaTheUIOrByReadarr": "سيتم استخدامه عند إجراء عمليات البحث التلقائي عبر واجهة المستخدم أو بواسطة Radarr",
|
||||
"SupportsSearchvalueWillBeUsedWhenAutomaticSearchesArePerformedViaTheUIOrByReadarr": "سيتم استخدامه عند إجراء عمليات البحث التلقائي عبر واجهة المستخدم أو بواسطة {appName}",
|
||||
"SupportsSearchvalueSearchIsNotSupportedWithThisIndexer": "البحث غير معتمد مع هذا المفهرس",
|
||||
"SupportsRssvalueRSSIsNotSupportedWithThisIndexer": "لا يتم دعم RSS مع هذا المفهرس",
|
||||
"SuccessMyWorkIsDoneNoFilesToRetag": "نجاح! تم الانتهاء من عملي ، ولا توجد ملفات لإعادة تسميتها.",
|
||||
@@ -92,7 +92,7 @@
|
||||
"Source": "مصدر",
|
||||
"SorryThatBookCannotBeFound": "آسف ، لا يمكن العثور على هذا الفيلم.",
|
||||
"SorryThatAuthorCannotBeFound": "آسف ، لا يمكن العثور على هذا الفيلم.",
|
||||
"SkipFreeSpaceCheckWhenImportingHelpText": "استخدم عندما يتعذر على Radarr اكتشاف مساحة خالية من مجلد جذر الفيلم",
|
||||
"SkipFreeSpaceCheckWhenImportingHelpText": "استخدم عندما يتعذر على {appName} اكتشاف مساحة خالية من مجلد جذر الفيلم",
|
||||
"SkipFreeSpaceCheck": "تخطي فحص المساحة الخالية",
|
||||
"Size": " بحجم",
|
||||
"ShownAboveEachColumnWhenWeekIsTheActiveView": "يظهر فوق كل عمود عندما يكون الأسبوع هو العرض النشط",
|
||||
@@ -133,19 +133,19 @@
|
||||
"Result": "نتيجة",
|
||||
"RestoreBackup": "استرجاع النسخة الاحتياطية",
|
||||
"Restore": "استعادة",
|
||||
"RestartReadarr": "أعد تشغيل Radarr",
|
||||
"RestartReadarr": "أعد تشغيل {appName}",
|
||||
"RestartNow": "اعد البدء الان",
|
||||
"Restart": "إعادة تشغيل",
|
||||
"ResetAPIKeyMessageText": "هل أنت متأكد أنك تريد إعادة تعيين مفتاح API الخاص بك؟",
|
||||
"ResetAPIKey": "إعادة تعيين مفتاح API",
|
||||
"Reset": "إعادة تعيين",
|
||||
"RescanAuthorFolderAfterRefresh": "إعادة فحص مجلد الفيلم بعد التحديث",
|
||||
"RescanAfterRefreshHelpTextWarning": "لن يكتشف Radarr تلقائيًا التغييرات التي تطرأ على الملفات عند عدم تعيينه على \"دائمًا\"",
|
||||
"RescanAfterRefreshHelpTextWarning": "لن يكتشف {appName} تلقائيًا التغييرات التي تطرأ على الملفات عند عدم تعيينه على \"دائمًا\"",
|
||||
"RequiredPlaceHolder": "أضف قيدًا جديدًا",
|
||||
"RequiredHelpText": "يجب أن يحتوي الإصدار على واحد على الأقل من هذه المصطلحات (غير حساس لحالة الأحرف)",
|
||||
"ReplaceIllegalCharacters": "استبدل الأحرف غير القانونية",
|
||||
"Reorder": "إعادة ترتيب",
|
||||
"RenameBooksHelpText": "سيستخدم Radarr اسم الملف الحالي إذا تم تعطيل إعادة التسمية",
|
||||
"RenameBooksHelpText": "سيستخدم {appName} اسم الملف الحالي إذا تم تعطيل إعادة التسمية",
|
||||
"RemovedFromTaskQueue": "تمت إزالته من قائمة انتظار المهام",
|
||||
"RemoveTagRemovingTag": "إزالة العلامة",
|
||||
"RemoveTagExistingTag": "علامة موجودة",
|
||||
@@ -174,7 +174,7 @@
|
||||
"Reason": "السبب",
|
||||
"Real": "حقيقة",
|
||||
"ReadarrTags": "العلامات الرادار",
|
||||
"ReadarrSupportsAnyIndexerThatUsesTheNewznabStandardAsWellAsOtherIndexersListedBelow": "يدعم Radarr أي مفهرس يستخدم معيار Newznab ، بالإضافة إلى مفهرسات أخرى مذكورة أدناه.",
|
||||
"ReadarrSupportsAnyIndexerThatUsesTheNewznabStandardAsWellAsOtherIndexersListedBelow": "يدعم {appName} أي مفهرس يستخدم معيار Newznab ، بالإضافة إلى مفهرسات أخرى مذكورة أدناه.",
|
||||
"ReadTheWikiForMoreInformation": "اقرأ Wiki لمزيد من المعلومات",
|
||||
"RSSSyncInterval": "الفاصل الزمني لمزامنة RSS",
|
||||
"RSSSync": "مزامنة RSS",
|
||||
@@ -196,7 +196,7 @@
|
||||
"Profiles": "مظهر",
|
||||
"PreviewRename": "معاينة إعادة تسمية",
|
||||
"PosterSize": "حجم الملصق",
|
||||
"GrabReleaseMessageText": "لم يتمكن Radarr من تحديد الفيلم الذي كان هذا الإصدار من أجله. قد يتعذر على Radarr استيراد هذا الإصدار تلقائيًا. هل تريد انتزاع \"{0}\"؟",
|
||||
"GrabReleaseMessageText": "لم يتمكن {appName} من تحديد الفيلم الذي كان هذا الإصدار من أجله. قد يتعذر على {appName} استيراد هذا الإصدار تلقائيًا. هل تريد انتزاع \"{0}\"؟",
|
||||
"GrabRelease": "انتزاع الإصدار",
|
||||
"GrabID": "انتزاع معرف",
|
||||
"Grab": "إختطاف",
|
||||
@@ -276,10 +276,10 @@
|
||||
"Dates": "تواريخ",
|
||||
"DatabaseMigration": "ترحيل DB",
|
||||
"CutoffUnmet": "قطع غير ملباة",
|
||||
"CutoffHelpText": "بمجرد الوصول إلى هذه الجودة ، لن يقوم Radarr بتنزيل الأفلام",
|
||||
"CutoffHelpText": "بمجرد الوصول إلى هذه الجودة ، لن يقوم {appName} بتنزيل الأفلام",
|
||||
"CreateGroup": "إنشاء مجموعة",
|
||||
"CreateEmptyAuthorFoldersHelpText": "قم بإنشاء مجلدات فيلم مفقودة أثناء فحص القرص",
|
||||
"CopyUsingHardlinksHelpTextWarning": "من حين لآخر ، قد تمنع أقفال الملفات إعادة تسمية الملفات التي يتم زرعها. يمكنك تعطيل البذر مؤقتًا واستخدام وظيفة إعادة تسمية Radarr كحل بديل.",
|
||||
"CopyUsingHardlinksHelpTextWarning": "من حين لآخر ، قد تمنع أقفال الملفات إعادة تسمية الملفات التي يتم زرعها. يمكنك تعطيل البذر مؤقتًا واستخدام وظيفة إعادة تسمية {appName} كحل بديل.",
|
||||
"CopyUsingHardlinksHelpText": "استخدم Hardlinks عند محاولة نسخ الملفات من السيول التي لا تزال تحت البذور",
|
||||
"Connections": "روابط",
|
||||
"ConnectSettings": "ربط الإعدادات",
|
||||
@@ -291,16 +291,16 @@
|
||||
"ClientPriority": "أولوية العميل",
|
||||
"ClickToChangeQuality": "انقر لتغيير الجودة",
|
||||
"Clear": "واضح",
|
||||
"ChownGroupHelpTextWarning": "يعمل هذا فقط إذا كان المستخدم الذي يقوم بتشغيل Radarr هو مالك الملف. من الأفضل التأكد من أن عميل التنزيل يستخدم نفس مجموعة Radarr.",
|
||||
"ChownGroupHelpTextWarning": "يعمل هذا فقط إذا كان المستخدم الذي يقوم بتشغيل {appName} هو مالك الملف. من الأفضل التأكد من أن عميل التنزيل يستخدم نفس مجموعة {appName}.",
|
||||
"ChownGroupHelpText": "اسم المجموعة أو gid. استخدم gid لأنظمة الملفات البعيدة.",
|
||||
"ChmodFolderHelpTextWarning": "يعمل هذا فقط إذا كان المستخدم الذي يقوم بتشغيل Radarr هو مالك الملف. من الأفضل التأكد من قيام عميل التنزيل بتعيين الأذونات بشكل صحيح.",
|
||||
"ChmodFolderHelpTextWarning": "يعمل هذا فقط إذا كان المستخدم الذي يقوم بتشغيل {appName} هو مالك الملف. من الأفضل التأكد من قيام عميل التنزيل بتعيين الأذونات بشكل صحيح.",
|
||||
"ChmodFolderHelpText": "Octal ، يتم تطبيقه أثناء الاستيراد / إعادة التسمية إلى مجلدات وملفات الوسائط (بدون تنفيذ بت)",
|
||||
"ChmodFolder": "مجلد chmod",
|
||||
"ChangeHasNotBeenSavedYet": "لم يتم حفظ التغيير بعد",
|
||||
"ChangeFileDate": "تغيير تاريخ الملف",
|
||||
"CertificateValidationHelpText": "تغيير مدى صرامة التحقق من صحة شهادة HTTPS",
|
||||
"CertificateValidation": "التحقق من صحة الشهادة",
|
||||
"CancelMessageText": "هل أنت متأكد أنك تريد إلغاء هذه المهمة المعلقة؟",
|
||||
"CancelPendingTask": "هل أنت متأكد أنك تريد إلغاء هذه المهمة المعلقة؟",
|
||||
"Cancel": "إلغاء",
|
||||
"CalendarWeekColumnHeaderHelpText": "يظهر فوق كل عمود عندما يكون الأسبوع هو العرض النشط",
|
||||
"Calendar": "التقويم",
|
||||
@@ -314,17 +314,17 @@
|
||||
"Backups": "النسخ الاحتياطية",
|
||||
"BackupRetentionHelpText": "سيتم تنظيف النسخ الاحتياطية التلقائية الأقدم من فترة الاحتفاظ تلقائيًا",
|
||||
"BackupNow": "اعمل نسخة احتياطية الان",
|
||||
"BackupFolderHelpText": "ستكون المسارات النسبية ضمن دليل AppData الخاص بـ Radarr",
|
||||
"BackupFolderHelpText": "ستكون المسارات النسبية ضمن دليل AppData الخاص بـ {appName}",
|
||||
"Automatic": "تلقائي",
|
||||
"AutoUnmonitorPreviouslyDownloadedBooksHelpText": "الأفلام المحذوفة من القرص لا يتم مراقبتها تلقائيًا في Radarr",
|
||||
"AutoUnmonitorPreviouslyDownloadedBooksHelpText": "الأفلام المحذوفة من القرص لا يتم مراقبتها تلقائيًا في {appName}",
|
||||
"AutoRedownloadFailedHelpText": "ابحث تلقائيًا عن إصدار مختلف وحاول تنزيله",
|
||||
"AuthorClickToChangeBook": "انقر لتغيير الفيلم",
|
||||
"AuthenticationMethodHelpText": "طلب اسم المستخدم وكلمة المرور للوصول إلى Radarr",
|
||||
"AuthenticationMethodHelpText": "طلب اسم المستخدم وكلمة المرور للوصول إلى {appName}",
|
||||
"Authentication": "المصادقة",
|
||||
"ApplyTags": "تطبيق العلامات",
|
||||
"AppDataDirectory": "دليل AppData",
|
||||
"AnalyticsEnabledHelpTextWarning": "يتطلب إعادة التشغيل ليصبح ساري المفعول",
|
||||
"AnalyticsEnabledHelpText": "إرسال معلومات الاستخدام والخطأ المجهولة إلى خوادم Radarr. يتضمن ذلك معلومات حول متصفحك ، وصفحات Radarr WebUI التي تستخدمها ، والإبلاغ عن الأخطاء بالإضافة إلى إصدار نظام التشغيل ووقت التشغيل. سنستخدم هذه المعلومات لتحديد أولويات الميزات وإصلاحات الأخطاء.",
|
||||
"AnalyticsEnabledHelpText": "إرسال معلومات الاستخدام والخطأ المجهولة إلى خوادم {appName}. يتضمن ذلك معلومات حول متصفحك ، وصفحات {appName} WebUI التي تستخدمها ، والإبلاغ عن الأخطاء بالإضافة إلى إصدار نظام التشغيل ووقت التشغيل. سنستخدم هذه المعلومات لتحديد أولويات الميزات وإصلاحات الأخطاء.",
|
||||
"Analytics": "تحليلات",
|
||||
"AlternateTitles": "عنوان بديل",
|
||||
"AlreadyInYourLibrary": "بالفعل في مكتبتك",
|
||||
@@ -332,7 +332,6 @@
|
||||
"AddingTag": "مضيفا العلامة",
|
||||
"AddListExclusion": "إضافة استبعاد قائمة",
|
||||
"About": "نبدة عن",
|
||||
"APIKey": "مفتاح API",
|
||||
"60MinutesSixty": "60 دقيقة: {0}",
|
||||
"45MinutesFourtyFive": "90 دقيقة: {0}",
|
||||
"20MinutesTwenty": "120 دقيقة: {0}",
|
||||
@@ -396,7 +395,7 @@
|
||||
"LogFiles": "ملفات الدخول",
|
||||
"Local": "محلي",
|
||||
"LoadingBookFilesFailed": "فشل تحميل ملفات الفيلم",
|
||||
"LaunchBrowserHelpText": " افتح مستعرض ويب وانتقل إلى صفحة Radarr الرئيسية عند بدء التطبيق.",
|
||||
"LaunchBrowserHelpText": " افتح مستعرض ويب وانتقل إلى صفحة {appName} الرئيسية عند بدء التطبيق.",
|
||||
"Language": "لغة",
|
||||
"IsTagUsedCannotBeDeletedWhileInUse": "لا يمكن حذفه أثناء الاستخدام",
|
||||
"IsCutoffUpgradeUntilThisQualityIsMetOrExceeded": "قم بالترقية حتى يتم تلبية هذه الجودة أو تجاوزها",
|
||||
@@ -407,7 +406,7 @@
|
||||
"IndexerPriority": "أولوية المفهرس",
|
||||
"Indexer": "مفهرس",
|
||||
"IncludeUnmonitored": "تضمين غير مراقب",
|
||||
"IncludeUnknownAuthorItemsHelpText": "إظهار العناصر بدون فيلم في قائمة الانتظار. يمكن أن يشمل ذلك الأفلام التي تمت إزالتها أو أي شيء آخر في فئة Radarr",
|
||||
"IncludeUnknownAuthorItemsHelpText": "إظهار العناصر بدون فيلم في قائمة الانتظار. يمكن أن يشمل ذلك الأفلام التي تمت إزالتها أو أي شيء آخر في فئة {appName}",
|
||||
"IncludeHealthWarningsHelpText": "قم بتضمين التحذيرات الصحية",
|
||||
"Importing": "استيراد",
|
||||
"ImportedTo": "مستورد إلى",
|
||||
@@ -430,7 +429,6 @@
|
||||
"HasPendingChangesNoChanges": "لا تغيرات",
|
||||
"Group": "مجموعة",
|
||||
"GrabSelected": "انتزاع المحدد",
|
||||
"ApiKeyHelpTextWarning": "يتطلب إعادة التشغيل ليصبح ساري المفعول",
|
||||
"DeleteRootFolderMessageText": "هل أنت متأكد أنك تريد حذف المفهرس \"{0}\"؟",
|
||||
"LoadingBooksFailed": "فشل تحميل ملفات الفيلم",
|
||||
"ProxyUsernameHelpText": "ما عليك سوى إدخال اسم مستخدم وكلمة مرور إذا كان أحدهما مطلوبًا. اتركها فارغة وإلا.",
|
||||
@@ -439,7 +437,7 @@
|
||||
"SslPortHelpTextWarning": "يتطلب إعادة التشغيل ليصبح ساري المفعول",
|
||||
"DownloadClientCheckDownloadingToRoot": "يقوم برنامج التنزيل {0} بوضع التنزيلات في المجلد الجذر {1}. يجب ألا تقوم بالتنزيل إلى مجلد جذر.",
|
||||
"Progress": "التقدم",
|
||||
"ReplaceIllegalCharactersHelpText": "استبدل الأحرف غير القانونية. إذا لم يتم تحديده ، فسوف يقوم Radarr بإزالتها بدلاً من ذلك",
|
||||
"ReplaceIllegalCharactersHelpText": "استبدل الأحرف غير القانونية. إذا لم يتم تحديده ، فسوف يقوم {appName} بإزالتها بدلاً من ذلك",
|
||||
"ReleaseTitle": "عنوان الإصدار",
|
||||
"Actions": "أجراءات",
|
||||
"Tomorrow": "غدا",
|
||||
@@ -454,12 +452,12 @@
|
||||
"RemoveFromBlocklist": "إزالة من القائمة السوداء",
|
||||
"UnableToLoadBlocklist": "تعذر تحميل القائمة السوداء",
|
||||
"Level": "مستوى",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "الفرع {0} ليس فرع إصدار Radarr صالح ، لن تتلقى تحديثات",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "الفرع {0} ليس فرع إصدار {appName} صالح ، لن تتلقى تحديثات",
|
||||
"Time": "زمن",
|
||||
"Component": "مكون",
|
||||
"Blocklist": "القائمة السوداء",
|
||||
"BlocklistRelease": "إصدار القائمة السوداء",
|
||||
"ThisCannotBeCancelled": "لا يمكن إلغاء هذا بمجرد البدء دون إعادة تشغيل Radarr.",
|
||||
"ThisCannotBeCancelled": "لا يمكن إلغاء هذا بمجرد البدء دون إعادة تشغيل {appName}.",
|
||||
"UnselectAll": "إلغاء تحديد الكل",
|
||||
"UpdateSelected": "تم تحديد التحديث",
|
||||
"Wanted": "مطلوب",
|
||||
@@ -485,12 +483,12 @@
|
||||
"Filters": "منقي",
|
||||
"Connect": "الاتصال",
|
||||
"HealthNoIssues": "لا مشاكل مع التكوين الخاص بك",
|
||||
"MissingFromDisk": "لم يتمكن Whisparr من العثور على الملف على القرص لذا تمت إزالته",
|
||||
"MissingFromDisk": "لم يتمكن {appName} من العثور على الملف على القرص لذا تمت إزالته",
|
||||
"IndexerStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات: {0}",
|
||||
"Lists": "القوائم",
|
||||
"Metadata": "البيانات الوصفية",
|
||||
"CreateEmptyAuthorFolders": "إنشاء مجلدات أفلام فارغة",
|
||||
"ReadarrSupportsAnyDownloadClient": "يدعم Whisparr أي عميل تنزيل يستخدم معيار Newznab ، بالإضافة إلى عملاء التنزيل الآخرين المدرجة أدناه.",
|
||||
"ReadarrSupportsAnyDownloadClient": "يدعم {appName} أي عميل تنزيل يستخدم معيار Newznab ، بالإضافة إلى عملاء التنزيل الآخرين المدرجة أدناه.",
|
||||
"Queued": "في قائمة الانتظار",
|
||||
"RefreshAndScan": "التحديث والمسح الضوئي",
|
||||
"RescanAfterRefreshHelpText": "أعد فحص مجلد الفيلم بعد تحديث الفيلم",
|
||||
@@ -509,10 +507,10 @@
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل لأكثر من 6 ساعات",
|
||||
"IndexerLongTermStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات لأكثر من 6 ساعات: {0}",
|
||||
"IndexerPriorityHelpText": "أولوية المفهرس من 1 (الأعلى) إلى 50 (الأدنى). الافتراضي: 25.",
|
||||
"IndexerRssHealthCheckNoIndexers": "لا توجد مفهرسات متاحة مع تمكين مزامنة RSS ، ولن يحصل Radarr على الإصدارات الجديدة تلقائيًا",
|
||||
"IndexerSearchCheckNoAutomaticMessage": "لا تتوفر مفهرسات مع تمكين البحث التلقائي ، ولن يقدم Radarr أي نتائج بحث تلقائية",
|
||||
"IndexerRssHealthCheckNoIndexers": "لا توجد مفهرسات متاحة مع تمكين مزامنة RSS ، ولن يحصل {appName} على الإصدارات الجديدة تلقائيًا",
|
||||
"IndexerSearchCheckNoAutomaticMessage": "لا تتوفر مفهرسات مع تمكين البحث التلقائي ، ولن يقدم {appName} أي نتائج بحث تلقائية",
|
||||
"IndexerSearchCheckNoAvailableIndexersMessage": "جميع المفهرسات القادرة على البحث غير متوفرة مؤقتًا بسبب أخطاء المفهرس الأخيرة",
|
||||
"IndexerSearchCheckNoInteractiveMessage": "لا تتوفر مفهرسات مع تمكين البحث التفاعلي ، ولن يقدم Radarr أي نتائج بحث تفاعلية",
|
||||
"IndexerSearchCheckNoInteractiveMessage": "لا تتوفر مفهرسات مع تمكين البحث التفاعلي ، ولن يقدم {appName} أي نتائج بحث تفاعلية",
|
||||
"IndexersSettingsSummary": "المفهرسات وقيود الإصدار",
|
||||
"MediaManagement": "إدارة وسائل الإعلام",
|
||||
"Monitor": "مراقب",
|
||||
@@ -522,11 +520,11 @@
|
||||
"ProxyCheckFailedToTestMessage": "فشل اختبار الوكيل: {0}",
|
||||
"QualitySettingsSummary": "أحجام الجودة والتسمية",
|
||||
"QueueIsEmpty": "قائمة الانتظار فارغة",
|
||||
"RestartReloadNote": "ملاحظة: سيتم إعادة تشغيل Radarr تلقائيًا وإعادة تحميل واجهة المستخدم أثناء عملية الاستعادة.",
|
||||
"RestartReloadNote": "ملاحظة: سيتم إعادة تشغيل {appName} تلقائيًا وإعادة تحميل واجهة المستخدم أثناء عملية الاستعادة.",
|
||||
"RootFolderCheckSingleMessage": "مجلد الجذر مفقود: {0}",
|
||||
"Save": "حفظ",
|
||||
"SettingsRemotePathMappingLocalPath": "مسار محلي",
|
||||
"SettingsRemotePathMappingLocalPathHelpText": "المسار الذي يجب أن يستخدمه Radarr للوصول إلى المسار البعيد محليًا",
|
||||
"SettingsRemotePathMappingLocalPathHelpText": "المسار الذي يجب أن يستخدمه {appName} للوصول إلى المسار البعيد محليًا",
|
||||
"SettingsRemotePathMappingRemotePath": "مسار بعيد",
|
||||
"DownloadClientsSettingsSummary": "تنزيل العملاء وتنزيل المناولة وتعيينات المسار البعيد",
|
||||
"DownloadClientStatusCheckAllClientMessage": "جميع عملاء التنزيل غير متاحين بسبب الفشل",
|
||||
@@ -556,7 +554,7 @@
|
||||
"CustomFormatSettings": "إعدادات التنسيقات المخصصة",
|
||||
"CustomFormat": "تنسيق مخصص",
|
||||
"CustomFormats": "تنسيقات مخصصة",
|
||||
"CutoffFormatScoreHelpText": "بمجرد الوصول إلى درجة التنسيق المخصص هذه ، لن يقوم Radarr بتنزيل الأفلام",
|
||||
"CutoffFormatScoreHelpText": "بمجرد الوصول إلى درجة التنسيق المخصص هذه ، لن يقوم {appName} بتنزيل الأفلام",
|
||||
"DeleteCustomFormat": "حذف التنسيق المخصص",
|
||||
"DeleteCustomFormatMessageText": "هل أنت متأكد أنك تريد حذف المفهرس \"{0}\"؟",
|
||||
"DeleteFormatMessageText": "هل تريد بالتأكيد حذف علامة التنسيق {0}؟",
|
||||
@@ -585,7 +583,7 @@
|
||||
"DeleteConditionMessageText": "هل أنت متأكد أنك تريد حذف العلامة \"{0}\"؟",
|
||||
"RemoveSelectedItemsQueueMessageText": "هل تريد بالتأكيد إزالة {0} عنصر {1} من قائمة الانتظار؟",
|
||||
"NoEventsFound": "لم يتم العثور على أحداث",
|
||||
"BlocklistReleaseHelpText": "يمنع Radarr من الاستيلاء على هذا الإصدار تلقائيًا مرة أخرى",
|
||||
"BlocklistReleaseHelpText": "يمنع {appName} من الاستيلاء على هذا الإصدار تلقائيًا مرة أخرى",
|
||||
"ApplyTagsHelpTextRemove": "إزالة: قم بإزالة العلامات التي تم إدخالها",
|
||||
"ApplyTagsHelpTextReplace": "استبدال: استبدل العلامات بالعلامات التي تم إدخالها (لا تدخل أي علامات لمسح جميع العلامات)",
|
||||
"DeleteSelectedDownloadClients": "حذف Download Client",
|
||||
@@ -617,7 +615,7 @@
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "القوائم غير متاحة بسبب الإخفاقات: {0}",
|
||||
"SomeResultsAreHiddenByTheAppliedFilter": "بعض النتائج مخفية بواسطة عامل التصفية المطبق",
|
||||
"ConnectionLost": "انقطع الاتصال",
|
||||
"ConnectionLostReconnect": "سيحاول Radarr الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه.",
|
||||
"ConnectionLostReconnect": "سيحاول {appName} الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه.",
|
||||
"LastDuration": "المدة الماضية",
|
||||
"Large": "كبير",
|
||||
"WhatsNew": "ما هو الجديد؟",
|
||||
@@ -638,5 +636,22 @@
|
||||
"SelectQuality": "حدد الجودة",
|
||||
"CustomFilter": "مرشحات مخصصة",
|
||||
"IndexerFlags": "أعلام المفهرس",
|
||||
"InteractiveSearchModalHeader": "بحث تفاعلي"
|
||||
"InteractiveSearchModalHeader": "بحث تفاعلي",
|
||||
"ApiKey": "مفتاح API",
|
||||
"AuthBasic": "أساسي (المتصفح المنبثق)",
|
||||
"AuthForm": "النماذج (صفحة تسجيل الدخول)",
|
||||
"Enabled": "ممكن",
|
||||
"FailedLoadingSearchResults": "فشل تحميل نتائج البحث ، يرجى المحاولة مرة أخرى.",
|
||||
"DisabledForLocalAddresses": "معطل بسبب العناوين المحلية",
|
||||
"AptUpdater": "استخدم apt لتثبيت التحديث",
|
||||
"CurrentlyInstalled": "مثبتة حاليا",
|
||||
"DockerUpdater": "تحديث حاوية عامل الإرساء لتلقي التحديث",
|
||||
"ExternalUpdater": "تم تكوين {appName} لاستخدام آلية تحديث خارجية",
|
||||
"InstallLatest": "تثبيت الأحدث",
|
||||
"OnLatestVersion": "تم بالفعل تثبيت أحدث إصدار من {appName}",
|
||||
"Script": "النصي",
|
||||
"UnmappedFiles": "المجلدات غير المعينة",
|
||||
"UpdateAppDirectlyLoadError": "تعذر تحديث {appName} مباشرة ،",
|
||||
"Clone": "قريب",
|
||||
"BuiltIn": "مدمج"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"ApiKeyHelpTextWarning": "Изисква рестартиране, за да влезе в сила",
|
||||
"Enable": "Активиране",
|
||||
"MIA": "МВР",
|
||||
"Size": " Размер",
|
||||
@@ -7,7 +6,6 @@
|
||||
"20MinutesTwenty": "120 минути: {0}",
|
||||
"45MinutesFourtyFive": "90 минути: {0}",
|
||||
"60MinutesSixty": "60 минути: {0}",
|
||||
"APIKey": "API ключ",
|
||||
"About": "относно",
|
||||
"AddListExclusion": "Добавяне на изключване от списъка",
|
||||
"AddingTag": "Добавяне на таг",
|
||||
@@ -15,17 +13,17 @@
|
||||
"AlreadyInYourLibrary": "Вече във вашата библиотека",
|
||||
"AlternateTitles": "Алтернативно заглавие",
|
||||
"Analytics": "Анализ",
|
||||
"AnalyticsEnabledHelpText": "Изпращайте анонимна информация за използването и грешките до сървърите на Radarr. Това включва информация за вашия браузър, кои страници на Radarr WebUI използвате, отчитане на грешки, както и версията на операционната система и времето за изпълнение. Ще използваме тази информация, за да дадем приоритет на функциите и корекциите на грешки.",
|
||||
"AnalyticsEnabledHelpText": "Изпращайте анонимна информация за използването и грешките до сървърите на {appName}. Това включва информация за вашия браузър, кои страници на {appName} WebUI използвате, отчитане на грешки, както и версията на операционната система и времето за изпълнение. Ще използваме тази информация, за да дадем приоритет на функциите и корекциите на грешки.",
|
||||
"AnalyticsEnabledHelpTextWarning": "Изисква рестартиране, за да влезе в сила",
|
||||
"AppDataDirectory": "Директория на AppData",
|
||||
"ApplyTags": "Прилагане на тагове",
|
||||
"Authentication": "Удостоверяване",
|
||||
"AuthenticationMethodHelpText": "Изисквайте потребителско име и парола за достъп до Radarr",
|
||||
"AuthenticationMethodHelpText": "Изисквайте потребителско име и парола за достъп до {appName}",
|
||||
"AuthorClickToChangeBook": "Щракнете, за да промените филма",
|
||||
"AutoRedownloadFailedHelpText": "Автоматично търсете и се опитвайте да изтеглите различна версия",
|
||||
"AutoUnmonitorPreviouslyDownloadedBooksHelpText": "Изтритите от диска филми автоматично се проследяват в Radarr",
|
||||
"AutoUnmonitorPreviouslyDownloadedBooksHelpText": "Изтритите от диска филми автоматично се проследяват в {appName}",
|
||||
"Automatic": "Автоматично",
|
||||
"BackupFolderHelpText": "Относителните пътища ще бъдат в директорията AppData на Radarr",
|
||||
"BackupFolderHelpText": "Относителните пътища ще бъдат в директорията AppData на {appName}",
|
||||
"BackupNow": "Архивиране сега",
|
||||
"BackupRetentionHelpText": "Автоматичните архиви, по-стари от периода на съхранение, ще бъдат почистени автоматично",
|
||||
"Backups": "Архиви",
|
||||
@@ -38,16 +36,16 @@
|
||||
"Calendar": "Календар",
|
||||
"CalendarWeekColumnHeaderHelpText": "Показва се над всяка колона, когато седмицата е активният изглед",
|
||||
"Cancel": "Отказ",
|
||||
"CancelMessageText": "Наистина ли искате да отмените тази чакаща задача?",
|
||||
"CancelPendingTask": "Наистина ли искате да отмените тази чакаща задача?",
|
||||
"CertificateValidation": "Валидиране на сертификат",
|
||||
"CertificateValidationHelpText": "Променете колко строго е валидирането на HTTPS сертифициране",
|
||||
"ChangeFileDate": "Промяна на датата на файла",
|
||||
"ChangeHasNotBeenSavedYet": "Промяната все още не е запазена",
|
||||
"ChmodFolder": "chmod папка",
|
||||
"ChmodFolderHelpText": "Осмично, приложено по време на импортиране / преименуване към медийни папки и файлове (без битове за изпълнение)",
|
||||
"ChmodFolderHelpTextWarning": "Това работи само ако потребителят, работещ с Radarr, е собственик на файла. По-добре е да се уверите, че клиентът за изтегляне правилно задава разрешенията.",
|
||||
"ChmodFolderHelpTextWarning": "Това работи само ако потребителят, работещ с {appName}, е собственик на файла. По-добре е да се уверите, че клиентът за изтегляне правилно задава разрешенията.",
|
||||
"ChownGroupHelpText": "Име на група или gid. Използвайте gid за отдалечени файлови системи.",
|
||||
"ChownGroupHelpTextWarning": "Това работи само ако потребителят, работещ с Radarr, е собственик на файла. По-добре е да се уверите, че клиентът за изтегляне използва същата група като Radarr.",
|
||||
"ChownGroupHelpTextWarning": "Това работи само ако потребителят, работещ с {appName}, е собственик на файла. По-добре е да се уверите, че клиентът за изтегляне използва същата група като {appName}.",
|
||||
"Clear": "Ясно",
|
||||
"ClickToChangeQuality": "Щракнете, за да промените качеството",
|
||||
"ClientPriority": "Приоритет на клиента",
|
||||
@@ -59,10 +57,10 @@
|
||||
"ConnectSettings": "Настройки за свързване",
|
||||
"Connections": "Връзки",
|
||||
"CopyUsingHardlinksHelpText": "Използвайте твърди връзки, когато се опитвате да копирате файлове от торенти, които все още се посяват",
|
||||
"CopyUsingHardlinksHelpTextWarning": "Понякога заключванията на файлове могат да попречат на преименуване на файлове, които се засяват. Можете временно да деактивирате засяването и да използвате функцията за преименуване на Radarr като работа.",
|
||||
"CopyUsingHardlinksHelpTextWarning": "Понякога заключванията на файлове могат да попречат на преименуване на файлове, които се засяват. Можете временно да деактивирате засяването и да използвате функцията за преименуване на {appName} като работа.",
|
||||
"CreateEmptyAuthorFoldersHelpText": "Създайте липсващи папки с филми по време на сканиране на диска",
|
||||
"CreateGroup": "Създай група",
|
||||
"CutoffHelpText": "След достигане на това качество Radarr вече няма да изтегля филми",
|
||||
"CutoffHelpText": "След достигане на това качество {appName} вече няма да изтегля филми",
|
||||
"CutoffUnmet": "Прекъсване Неудовлетворено",
|
||||
"DatabaseMigration": "DB миграция",
|
||||
"Dates": "Дати",
|
||||
@@ -141,7 +139,7 @@
|
||||
"Grab": "Грабнете",
|
||||
"GrabID": "Идентификатор на грабване",
|
||||
"GrabRelease": "Grab Release",
|
||||
"GrabReleaseMessageText": "Radarr не успя да определи за кой филм е предназначено това издание. Radarr може да не може автоматично да импортира тази версия. Искате ли да вземете „{0}“?",
|
||||
"GrabReleaseMessageText": "{appName} не успя да определи за кой филм е предназначено това издание. {appName} може да не може автоматично да импортира тази версия. Искате ли да вземете „{0}“?",
|
||||
"GrabSelected": "Grab Selected",
|
||||
"Group": "Група",
|
||||
"HasPendingChangesNoChanges": "Без промени",
|
||||
@@ -164,7 +162,7 @@
|
||||
"ImportedTo": "Внос в",
|
||||
"Importing": "Импортиране",
|
||||
"IncludeHealthWarningsHelpText": "Включете здравни предупреждения",
|
||||
"IncludeUnknownAuthorItemsHelpText": "Показване на елементи без филм в опашката. Това може да включва премахнати филми или нещо друго от категорията на Radarr",
|
||||
"IncludeUnknownAuthorItemsHelpText": "Показване на елементи без филм в опашката. Това може да включва премахнати филми или нещо друго от категорията на {appName}",
|
||||
"IncludeUnmonitored": "Включете Без наблюдение",
|
||||
"Indexer": "Индексатор",
|
||||
"IndexerPriority": "Индексатор Приоритет",
|
||||
@@ -175,7 +173,7 @@
|
||||
"IsCutoffUpgradeUntilThisQualityIsMetOrExceeded": "Надстройте, докато това качество бъде постигнато или надвишено",
|
||||
"IsTagUsedCannotBeDeletedWhileInUse": "Не може да се изтрие, докато се използва",
|
||||
"Language": "Език",
|
||||
"LaunchBrowserHelpText": " Отворете уеб браузър и отворете началната страница на Radarr при стартиране на приложението.",
|
||||
"LaunchBrowserHelpText": " Отворете уеб браузър и отворете началната страница на {appName} при стартиране на приложението.",
|
||||
"LoadingBookFilesFailed": "Зареждането на филмови файлове не бе успешно",
|
||||
"Local": "Местен",
|
||||
"LogFiles": "Лог файлове",
|
||||
@@ -256,7 +254,7 @@
|
||||
"RSSSync": "RSS синхронизиране",
|
||||
"RSSSyncInterval": "RSS интервал за синхронизиране",
|
||||
"ReadTheWikiForMoreInformation": "Прочетете Wiki за повече информация",
|
||||
"ReadarrSupportsAnyIndexerThatUsesTheNewznabStandardAsWellAsOtherIndexersListedBelow": "Radarr поддържа всеки индексатор, който използва стандарта Newznab, както и други индексатори, изброени по-долу.",
|
||||
"ReadarrSupportsAnyIndexerThatUsesTheNewznabStandardAsWellAsOtherIndexersListedBelow": "{appName} поддържа всеки индексатор, който използва стандарта Newznab, както и други индексатори, изброени по-долу.",
|
||||
"ReadarrTags": "Радарни маркери",
|
||||
"Real": "Истински",
|
||||
"Reason": "Причина",
|
||||
@@ -285,19 +283,19 @@
|
||||
"RemoveTagExistingTag": "Съществуващ маркер",
|
||||
"RemoveTagRemovingTag": "Премахване на етикет",
|
||||
"RemovedFromTaskQueue": "Премахнато от опашката на задачите",
|
||||
"RenameBooksHelpText": "Radarr ще използва съществуващото име на файл, ако преименуването е деактивирано",
|
||||
"RenameBooksHelpText": "{appName} ще използва съществуващото име на файл, ако преименуването е деактивирано",
|
||||
"Reorder": "Пренареждане",
|
||||
"ReplaceIllegalCharacters": "Заменете незаконните символи",
|
||||
"RequiredHelpText": "Освобождаването трябва да съдържа поне един от тези термини (без регистрация)",
|
||||
"RequiredPlaceHolder": "Добавете ново ограничение",
|
||||
"RescanAfterRefreshHelpTextWarning": "Radarr няма автоматично да открива промени във файлове, когато не е зададено на „Винаги“",
|
||||
"RescanAfterRefreshHelpTextWarning": "{appName} няма автоматично да открива промени във файлове, когато не е зададено на „Винаги“",
|
||||
"RescanAuthorFolderAfterRefresh": "Повторно сканиране на папка за филм след опресняване",
|
||||
"Reset": "Нулиране",
|
||||
"ResetAPIKey": "Нулиране на API ключ",
|
||||
"ResetAPIKeyMessageText": "Наистина ли искате да нулирате своя API ключ?",
|
||||
"Restart": "Рестартирам",
|
||||
"RestartNow": "Рестартирай сега",
|
||||
"RestartReadarr": "Рестартирайте Radarr",
|
||||
"RestartReadarr": "Рестартирайте {appName}",
|
||||
"Restore": "Възстанови",
|
||||
"RestoreBackup": "Възстанови архива",
|
||||
"Result": "Резултат",
|
||||
@@ -337,7 +335,7 @@
|
||||
"ShowSizeOnDisk": "Показване на размера на диска",
|
||||
"ShownAboveEachColumnWhenWeekIsTheActiveView": "Показва се над всяка колона, когато седмицата е активният изглед",
|
||||
"SkipFreeSpaceCheck": "Пропуснете проверката на свободното пространство",
|
||||
"SkipFreeSpaceCheckWhenImportingHelpText": "Използвайте, когато Radarr не е в състояние да открие свободно място от основната папка на вашия филм",
|
||||
"SkipFreeSpaceCheckWhenImportingHelpText": "Използвайте, когато {appName} не е в състояние да открие свободно място от основната папка на вашия филм",
|
||||
"SorryThatAuthorCannotBeFound": "За съжаление този филм не може да бъде намерен.",
|
||||
"SorryThatBookCannotBeFound": "За съжаление този филм не може да бъде намерен.",
|
||||
"Source": "Източник",
|
||||
@@ -355,7 +353,7 @@
|
||||
"SuccessMyWorkIsDoneNoFilesToRetag": "Успех! Работата ми приключи, няма файлове за преименуване.",
|
||||
"SupportsRssvalueRSSIsNotSupportedWithThisIndexer": "RSS не се поддържа с този индексатор",
|
||||
"SupportsSearchvalueSearchIsNotSupportedWithThisIndexer": "Търсенето не се поддържа с този индексатор",
|
||||
"SupportsSearchvalueWillBeUsedWhenAutomaticSearchesArePerformedViaTheUIOrByReadarr": "Ще се използва, когато се извършват автоматични търсения чрез потребителския интерфейс или от Radarr",
|
||||
"SupportsSearchvalueWillBeUsedWhenAutomaticSearchesArePerformedViaTheUIOrByReadarr": "Ще се използва, когато се извършват автоматични търсения чрез потребителския интерфейс или от {appName}",
|
||||
"SupportsSearchvalueWillBeUsedWhenInteractiveSearchIsUsed": "Ще се използва, когато се използва интерактивно търсене",
|
||||
"TagIsNotUsedAndCanBeDeleted": "Етикетът не се използва и може да бъде изтрит",
|
||||
"Tags": "Етикети",
|
||||
@@ -372,7 +370,7 @@
|
||||
"Torrents": "Торенти",
|
||||
"TotalFileSize": "Общ размер на файла",
|
||||
"UILanguage": "Език на потребителския интерфейс",
|
||||
"UILanguageHelpText": "Език, който Radarr ще използва за потребителски интерфейс",
|
||||
"UILanguageHelpText": "Език, който {appName} ще използва за потребителски интерфейс",
|
||||
"UILanguageHelpTextWarning": "Необходимо е презареждане на браузъра",
|
||||
"UISettings": "Настройки на потребителския интерфейс",
|
||||
"UnableToAddANewDownloadClientPleaseTryAgain": "Не може да се добави нов клиент за изтегляне, моля, опитайте отново.",
|
||||
@@ -412,7 +410,7 @@
|
||||
"UnmonitoredHelpText": "Включете непроменени филми в iCal емисията",
|
||||
"UpdateAll": "Актуализирай всички",
|
||||
"UpdateAutomaticallyHelpText": "Автоматично изтегляне и инсталиране на актуализации. Все още ще можете да инсталирате от System: Updates",
|
||||
"UpdateMechanismHelpText": "Използвайте вградения в Radarr актуализатор или скрипт",
|
||||
"UpdateMechanismHelpText": "Използвайте вградения в {appName} актуализатор или скрипт",
|
||||
"UpdateScriptPathHelpText": "Път към персонализиран скрипт, който взема извлечен пакет за актуализация и обработва останалата част от процеса на актуализация",
|
||||
"Updates": "Актуализации",
|
||||
"UpgradeAllowedHelpText": "Ако инвалидните качества няма да бъдат надградени",
|
||||
@@ -424,8 +422,8 @@
|
||||
"UsenetDelay": "Usenet Delay",
|
||||
"UsenetDelayHelpText": "Забавете за минути, за да изчакате, преди да вземете съобщение от Usenet",
|
||||
"Username": "Потребителско име",
|
||||
"UsingExternalUpdateMechanismBranchToUseToUpdateReadarr": "Клон, който да се използва за актуализиране на Radarr",
|
||||
"UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism": "Клон, използван от външен механизъм за актуализация",
|
||||
"BranchUpdate": "Клон, който да се използва за актуализиране на {appName}",
|
||||
"BranchUpdateMechanism": "Клон, използван от външен механизъм за актуализация",
|
||||
"Version": "Версия",
|
||||
"WeekColumnHeader": "Заглавка на колоната на седмицата",
|
||||
"Year": "Година",
|
||||
@@ -438,7 +436,7 @@
|
||||
"UnableToLoadMetadataProfiles": "Профилите за забавяне не могат да се заредят",
|
||||
"SslCertPathHelpTextWarning": "Изисква рестартиране, за да влезе в сила",
|
||||
"DownloadClientCheckDownloadingToRoot": "Клиентът за изтегляне {0} поставя изтеглянията в основната папка {1}. Не трябва да изтегляте в основна папка.",
|
||||
"ReplaceIllegalCharactersHelpText": "Заменете незаконните символи. Ако не е отметнато, Radarr ще ги премахне вместо това",
|
||||
"ReplaceIllegalCharactersHelpText": "Заменете незаконните символи. Ако не е отметнато, {appName} ще ги премахне вместо това",
|
||||
"Actions": "Действия",
|
||||
"Tomorrow": "Утре",
|
||||
"Today": "Днес",
|
||||
@@ -455,12 +453,12 @@
|
||||
"RemoveFromBlocklist": "Премахване от черния списък",
|
||||
"Time": "Време",
|
||||
"UnableToLoadBlocklist": "Черният списък не може да се зареди",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "Клон {0} не е валиден клон за издаване на Radarr, няма да получавате актуализации",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "Клон {0} не е валиден клон за издаване на {appName}, няма да получавате актуализации",
|
||||
"Blocklist": "Черен списък",
|
||||
"BlocklistRelease": "Освобождаване на черния списък",
|
||||
"SelectAll": "Избери всичко",
|
||||
"SelectedCountBooksSelectedInterp": "{0} Избран / и филм / и",
|
||||
"ThisCannotBeCancelled": "Това не може да бъде отменено след стартиране без рестартиране на Radarr.",
|
||||
"ThisCannotBeCancelled": "Това не може да бъде отменено след стартиране без рестартиране на {appName}.",
|
||||
"UnselectAll": "Деселектирайте всички",
|
||||
"UpdateSelected": "Избрана актуализация",
|
||||
"Wanted": "Издирва се",
|
||||
@@ -488,12 +486,12 @@
|
||||
"ImportListStatusCheckSingleClientMessage": "Списъци, недостъпни поради неуспехи: {0}",
|
||||
"IndexerPriorityHelpText": "Приоритет на индексатора от 1 (най-висок) до 50 (най-нисък). По подразбиране: 25.",
|
||||
"IndexerRssHealthCheckNoAvailableIndexers": "Всички rss-съвместими индексатори са временно недостъпни поради скорошни грешки в индексатора",
|
||||
"IndexerRssHealthCheckNoIndexers": "Няма налични индексатори с активирана RSS синхронизация, Radarr няма да взема автоматично нови версии",
|
||||
"IndexerRssHealthCheckNoIndexers": "Няма налични индексатори с активирана RSS синхронизация, {appName} няма да взема автоматично нови версии",
|
||||
"Lists": "Списъци",
|
||||
"QueueIsEmpty": "Опашката е празна",
|
||||
"RefreshAndScan": "Опресняване и сканиране",
|
||||
"RescanAfterRefreshHelpText": "Повторно сканиране на папката с филм след освежаване на филма",
|
||||
"RestartReloadNote": "Забележка: Radarr автоматично ще рестартира и презареди потребителския интерфейс по време на процеса на възстановяване.",
|
||||
"RestartReloadNote": "Забележка: {appName} автоматично ще рестартира и презареди потребителския интерфейс по време на процеса на възстановяване.",
|
||||
"SearchFiltered": "Търсене Филтрирано",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "Не може да се комуникира с {0}.",
|
||||
"Connect": "Свържете",
|
||||
@@ -503,10 +501,10 @@
|
||||
"HealthNoIssues": "Няма проблеми с вашата конфигурация",
|
||||
"DownloadClientCheckNoneAvailableMessage": "Няма наличен клиент за изтегляне",
|
||||
"IndexersSettingsSummary": "Индексатори и ограничения за освобождаване",
|
||||
"MissingFromDisk": "Whisparr не можа да намери файла на диска, така че той беше премахнат",
|
||||
"MissingFromDisk": "{appName} не можа да намери файла на диска, така че той беше премахнат",
|
||||
"MediaManagement": "Управление на медиите",
|
||||
"Metadata": "Метаданни",
|
||||
"ReadarrSupportsAnyDownloadClient": "Whisparr поддържа всеки клиент за изтегляне, който използва стандарта Newznab, както и други клиенти за изтегляне, изброени по-долу.",
|
||||
"ReadarrSupportsAnyDownloadClient": "{appName} поддържа всеки клиент за изтегляне, който използва стандарта Newznab, както и други клиенти за изтегляне, изброени по-долу.",
|
||||
"RootFolderCheckSingleMessage": "Липсваща основна папка: {0}",
|
||||
"SettingsRemotePathMappingLocalPath": "Местен път",
|
||||
"SettingsRemotePathMappingRemotePath": "Отдалечен път",
|
||||
@@ -514,9 +512,9 @@
|
||||
"Yesterday": "Вчера",
|
||||
"Disabled": "хора с увреждания",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Всички индексатори са недостъпни поради грешки за повече от 6 часа",
|
||||
"IndexerSearchCheckNoAutomaticMessage": "Няма налични индексатори с активирано автоматично търсене, Radarr няма да предоставя резултати от автоматично търсене",
|
||||
"IndexerSearchCheckNoAutomaticMessage": "Няма налични индексатори с активирано автоматично търсене, {appName} няма да предоставя резултати от автоматично търсене",
|
||||
"IndexerSearchCheckNoAvailableIndexersMessage": "Всички индексатори с възможност за търсене са временно недостъпни поради скорошни грешки в индексатора",
|
||||
"IndexerSearchCheckNoInteractiveMessage": "Няма налични индексатори с активирано интерактивно търсене, Radarr няма да предоставя никакви интерактивни резултати от търсенето",
|
||||
"IndexerSearchCheckNoInteractiveMessage": "Няма налични индексатори с активирано интерактивно търсене, {appName} няма да предоставя никакви интерактивни резултати от търсенето",
|
||||
"IndexerStatusCheckAllClientMessage": "Всички индексатори са недостъпни поради грешки",
|
||||
"IndexerStatusCheckSingleClientMessage": "Индексатори не са налични поради грешки: {0}",
|
||||
"MaintenanceRelease": "Издание за поддръжка: поправки на грешки и други подобрения. Вижте История на комисиите на Github за повече подробности",
|
||||
@@ -531,7 +529,7 @@
|
||||
"ProxyCheckFailedToTestMessage": "Неуспешно тестване на прокси: {0}",
|
||||
"ProxyCheckResolveIpMessage": "Неуспешно разрешаване на IP адреса за конфигурирания прокси хост {0}",
|
||||
"Queued": "На опашка",
|
||||
"SettingsRemotePathMappingLocalPathHelpText": "Път, който Radarr трябва да използва за локален достъп до отдалечения път",
|
||||
"SettingsRemotePathMappingLocalPathHelpText": "Път, който {appName} трябва да използва за локален достъп до отдалечения път",
|
||||
"SettingsRemotePathMappingRemotePathHelpText": "Основен път към директорията, до която клиентът за изтегляне има достъп",
|
||||
"ShowUnknownAuthorItems": "Показване на непознати филмови елементи",
|
||||
"SystemTimeCheckMessage": "Системното време е изключено с повече от 1 ден. Планираните задачи може да не се изпълняват правилно, докато времето не бъде коригирано",
|
||||
@@ -556,7 +554,7 @@
|
||||
"CustomFormat": "Персонализиран формат",
|
||||
"CustomFormatSettings": "Настройки на персонализирани формати",
|
||||
"CustomFormats": "Персонализирани формати",
|
||||
"CutoffFormatScoreHelpText": "След като бъде постигнат резултатът от този персонализиран формат, Radarr вече няма да изтегля филми",
|
||||
"CutoffFormatScoreHelpText": "След като бъде постигнат резултатът от този персонализиран формат, {appName} вече няма да изтегля филми",
|
||||
"DeleteCustomFormat": "Изтриване на потребителски формат",
|
||||
"DeleteFormatMessageText": "Наистина ли искате да изтриете маркер за формат {0}?",
|
||||
"ExportCustomFormat": "Експортиране на потребителски формат",
|
||||
@@ -606,7 +604,7 @@
|
||||
"Small": "Малък",
|
||||
"System": "Система",
|
||||
"Ui": "Потребителски интерфейс",
|
||||
"ConnectionLostReconnect": "Radarr ще се опита да се свърже автоматично или можете да щракнете върху презареждане по-долу.",
|
||||
"ConnectionLostReconnect": "{appName} ще се опита да се свърже автоматично или можете да щракнете върху презареждане по-долу.",
|
||||
"TotalSpace": "Общо пространство",
|
||||
"Events": "Събития",
|
||||
"Large": "Голям",
|
||||
@@ -638,5 +636,22 @@
|
||||
"CustomFilter": "Персонализирани филтри",
|
||||
"RemoveQueueItemConfirmation": "Наистина ли искате да премахнете {0} елемент {1} от опашката?",
|
||||
"IndexerFlags": "Индексиращи знамена",
|
||||
"InteractiveSearchModalHeader": "Интерактивно търсене"
|
||||
"InteractiveSearchModalHeader": "Интерактивно търсене",
|
||||
"FailedLoadingSearchResults": "Неуспешно зареждане на резултатите от търсенето, моля, опитайте отново.",
|
||||
"ApiKey": "API ключ",
|
||||
"AuthBasic": "Основно (изскачащ прозорец на браузъра)",
|
||||
"AuthForm": "Формуляри (Страница за вход)",
|
||||
"DisabledForLocalAddresses": "Забранено за местни адреси",
|
||||
"Enabled": "Активирано",
|
||||
"AptUpdater": "Използвайте apt, за да инсталирате актуализацията",
|
||||
"BuiltIn": "Вграден",
|
||||
"CurrentlyInstalled": "Понастоящем инсталиран",
|
||||
"ExternalUpdater": "{appName} е конфигуриран да използва външен механизъм за актуализация",
|
||||
"Script": "Сценарий",
|
||||
"UnmappedFiles": "Немапирани папки",
|
||||
"UpdateAppDirectlyLoadError": "Не може да се актуализира {appName} директно,",
|
||||
"Clone": "Близо",
|
||||
"DockerUpdater": "актуализирайте контейнера на докера, за да получите актуализацията",
|
||||
"InstallLatest": "Инсталирайте най-новите",
|
||||
"OnLatestVersion": "Вече е инсталирана най-новата версия на {appName}"
|
||||
}
|
||||
|
||||
@@ -428,25 +428,25 @@
|
||||
"BlocklistRelease": "Publicació de la llista de bloqueig",
|
||||
"HasPendingChangesNoChanges": "Sense Canvis",
|
||||
"ManualImportSelectEdition": "Importació manual - Seleccioneu la pel·lícula",
|
||||
"MissingFromDisk": "Radarr no ha pogut trobar el fitxer al disc, de manera que el fitxer es desenllaçarà de la pel·lícula a la base de dades",
|
||||
"MissingFromDisk": "{appName} no ha pogut trobar el fitxer al disc, de manera que el fitxer es desenllaçarà de la pel·lícula a la base de dades",
|
||||
"SupportsRssvalueRSSIsNotSupportedWithThisIndexer": "RSS no és compatible amb aquest indexador",
|
||||
"SupportsSearchvalueWillBeUsedWhenAutomaticSearchesArePerformedViaTheUIOrByReadarr": "S'utilitzarà quan es realitzin cerques automàtiques mitjançant la interfície d'usuari o per Radarr",
|
||||
"CutoffHelpText": "Un cop s'assoleixi aquesta qualitat, Radarr ja no baixarà pel·lícules",
|
||||
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau de l'API?",
|
||||
"SupportsSearchvalueWillBeUsedWhenAutomaticSearchesArePerformedViaTheUIOrByReadarr": "S'utilitzarà quan es realitzin cerques automàtiques mitjançant la interfície d'usuari o per {appName}",
|
||||
"CutoffHelpText": "Un cop s'assoleixi aquesta qualitat, {appName} ja no baixarà pel·lícules",
|
||||
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau API?",
|
||||
"PropersAndRepacks": "Propietats i Repacks",
|
||||
"RemotePathMappingCheckFolderPermissions": "Radarr pot veure però no accedir al directori de descàrregues {0}. Error de permisos probable.",
|
||||
"RemotePathMappingCheckFolderPermissions": "{appName} pot veure però no accedir al directori de descàrregues {0}. Error de permisos probable.",
|
||||
"RescanAuthorFolderAfterRefresh": "Torna a escanejar la carpeta de pel·lícules després de l'actualització",
|
||||
"RescanAfterRefreshHelpText": "Torneu a escanejar la carpeta de la pel·lícula després d'actualitzar la pel·lícula",
|
||||
"RestartReadarr": "Reinicia Radarr",
|
||||
"RestartReadarr": "Reinicia {appName}",
|
||||
"ShowRelativeDatesHelpText": "Mostra dates relatives (avui/ahir/etc) o absolutes",
|
||||
"ShowSearchActionHelpText": "Mostra el botó de cerca al passar el cursor",
|
||||
"TheAuthorFolderAndAllOfItsContentWillBeDeleted": "La carpeta de pel·lícules '{0}' i tot el seu contingut es suprimiran.",
|
||||
"UrlBaseHelpTextWarning": "Cal reiniciar perquè tingui efecte",
|
||||
"ApplicationURL": "URL de l'aplicació",
|
||||
"ApplicationUrlHelpText": "URL extern d'aquesta aplicació, inclòs http(s)://, port i URL base",
|
||||
"BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData del Radarr",
|
||||
"CancelMessageText": "Esteu segur que voleu cancel·lar aquesta tasca pendent?",
|
||||
"ChownGroupHelpTextWarning": "Això només funciona si l'usuari que executa Radarr és el propietari del fitxer. És millor assegurar-se que el client de descàrrega utilitza el mateix grup que Radarr.",
|
||||
"ApplicationUrlHelpText": "URL extern de l'aplicació, inclòs http(s)://, port i URL base",
|
||||
"BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData del {appName}",
|
||||
"CancelPendingTask": "Esteu segur que voleu cancel·lar aquesta tasca pendent?",
|
||||
"ChownGroupHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega utilitza el mateix grup que {appName}.",
|
||||
"ConnectSettingsSummary": "Notificacions, connexions a servidors/reproductors multimèdia i scripts personalitzats",
|
||||
"DeleteEmptyFoldersHelpText": "Suprimeix les carpetes de pel·lícules buides durant l'exploració del disc i quan s'esborren els fitxers de pel·lícules",
|
||||
"DeleteImportListMessageText": "Esteu segur que voleu suprimir la llista '{name}'?",
|
||||
@@ -454,26 +454,26 @@
|
||||
"ForMoreInformationOnTheIndividualDownloadClientsClickOnTheInfoButtons": "Per obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.",
|
||||
"ForMoreInformationOnTheIndividualIndexersClickOnTheInfoButtons": "Per obtenir més informació sobre els indexadors individuals, feu clic als botons d'informació.",
|
||||
"ForMoreInformationOnTheIndividualListsClickOnTheInfoButtons": "Per obtenir més informació sobre les llistes d'importació individuals, feu clic als botons d'informació.",
|
||||
"IndexerPriorityHelpText": "Prioritat de l'indexador d'1 (la més alta) a 50 (la més baixa). Per defecte: 25. S'utilitza quan s'agafa llançaments com a desempat per a versions iguals, Radarr encara utilitzarà tots els indexadors habilitats per a la sincronització i la cerca RSS",
|
||||
"IndexerRssHealthCheckNoIndexers": "No hi ha indexadors disponibles amb la sincronització RSS activada, Radarr no capturarà les noves versions automàticament",
|
||||
"IndexerSearchCheckNoAutomaticMessage": "No hi ha indexadors disponibles amb la cerca automàtica activada, Radarr no proporcionarà cap resultat de cerca automàtica",
|
||||
"IndexerSearchCheckNoInteractiveMessage": "No hi ha indexadors amb la cerca interactiva activada, Radarr no obtindrà cap resultat de cerca",
|
||||
"IndexerPriorityHelpText": "Prioritat de l'indexador d'1 (la més alta) a 50 (la més baixa). Per defecte: 25. S'utilitza quan s'agafa llançaments com a desempat per a versions iguals, {appName} encara utilitzarà tots els indexadors habilitats per a la sincronització i la cerca RSS",
|
||||
"IndexerRssHealthCheckNoIndexers": "No hi ha indexadors disponibles amb la sincronització RSS activada, {appName} no capturarà les noves versions automàticament",
|
||||
"IndexerSearchCheckNoAutomaticMessage": "No hi ha indexadors disponibles amb la cerca automàtica activada, {appName} no proporcionarà cap resultat de cerca automàtica",
|
||||
"IndexerSearchCheckNoInteractiveMessage": "No hi ha indexadors amb la cerca interactiva activada, {appName} no obtindrà cap resultat de cerca",
|
||||
"IsCutoffUpgradeUntilThisQualityIsMetOrExceeded": "Actualitzeu fins que s'assoleixi o superi aquesta qualitat",
|
||||
"IsTagUsedCannotBeDeletedWhileInUse": "No es pot suprimir mentre està en ús",
|
||||
"LaunchBrowserHelpText": " Obriu un navegador web i navegueu a la pàgina d'inici de Lidarr a l'inici de l'aplicació.",
|
||||
"LaunchBrowserHelpText": " Obriu un navegador web i navegueu a la pàgina d'inici de {appName} a l'inici de l'aplicació.",
|
||||
"LoadingBookFilesFailed": "No s'han pogut carregar els fitxers de pel·lícules",
|
||||
"NoHistory": "Sense història",
|
||||
"OnBookFileDeleteForUpgradeHelpText": "Al suprimir el fitxer de pel·lícula per a l'actualització",
|
||||
"OnBookFileDeleteHelpText": "Al suprimir fitxer de pel·lícula",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "La branca {0} no és una branca de llançament de Radarr vàlida, no rebreu actualitzacions",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "La branca {0} no és una branca de llançament de {appName} vàlida, no rebreu actualitzacions",
|
||||
"ReleaseDate": "Dates de llançament",
|
||||
"RemotePathMappingCheckDownloadPermissions": "Radarr pot veure però no accedir a la pel·lícula baixada {0}. Error de permisos probable.",
|
||||
"RemotePathMappingCheckFilesGenericPermissions": "El client de baixada {0} ha informat de fitxers a {1} però Radarr no pot veure aquest directori. És possible que hàgiu d'ajustar els permisos de la carpeta.",
|
||||
"RemotePathMappingCheckGenericPermissions": "El client de baixada {0} col·loca les baixades a {1} però Radarr no pot veure aquest directori. És possible que hàgiu d'ajustar els permisos de la carpeta.",
|
||||
"ReplaceIllegalCharactersHelpText": "Substitueix caràcters il·legals. Si no es marca, Radarr els eliminarà",
|
||||
"RemotePathMappingCheckDownloadPermissions": "{appName} pot veure però no accedir a la pel·lícula baixada {0}. Error de permisos probable.",
|
||||
"RemotePathMappingCheckFilesGenericPermissions": "El client de baixada {0} ha informat de fitxers a {1} però {appName} no pot veure aquest directori. És possible que hàgiu d'ajustar els permisos de la carpeta.",
|
||||
"RemotePathMappingCheckGenericPermissions": "El client de baixada {0} col·loca les baixades a {1} però {appName} no pot veure aquest directori. És possible que hàgiu d'ajustar els permisos de la carpeta.",
|
||||
"ReplaceIllegalCharactersHelpText": "Substitueix caràcters il·legals. Si no es marca, {appName} els eliminarà",
|
||||
"RssSyncIntervalHelpText": "Interval en minuts. Establiu a zero per desactivar (això aturarà tota captura automàtica de llançaments)",
|
||||
"SelectedCountBooksSelectedInterp": "S'han seleccionat {0} pel·lícules",
|
||||
"SettingsRemotePathMappingLocalPathHelpText": "Camí que Radarr hauria d'utilitzar per accedir al camí remot localment",
|
||||
"SettingsRemotePathMappingLocalPathHelpText": "Camí que {appName} hauria d'utilitzar per accedir al camí remot localment",
|
||||
"ShortDateFormat": "Format de data curta",
|
||||
"ShowBookTitleHelpText": "Mostra el títol de la pel·lícula sota el cartell",
|
||||
"ShowRelativeDates": "Mostra les dates relatives",
|
||||
@@ -487,11 +487,11 @@
|
||||
"SuccessMyWorkIsDoneNoFilesToRetag": "Èxit! La feina està acabada, no hi ha fitxers per canviar el nom.",
|
||||
"TimeLeft": "Temps restant",
|
||||
"UnableToLoadImportListExclusions": "No es poden carregar les exclusions de la llista",
|
||||
"UsingExternalUpdateMechanismBranchToUseToUpdateReadarr": "Branca que s'utilitza per actualitzar Radarr",
|
||||
"BranchUpdate": "Branca que s'utilitza per actualitzar {appName}",
|
||||
"UserAgentProvidedByTheAppThatCalledTheAPI": "Agent d'usuari proporcionat per l'aplicació per fer peticions a l'API",
|
||||
"UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism": "Branca utilitzada pel mecanisme d'actualització extern",
|
||||
"BranchUpdateMechanism": "Branca utilitzada pel mecanisme d'actualització extern",
|
||||
"WriteTagsNo": "Mai",
|
||||
"RestartReloadNote": "Nota: Radarr es reiniciarà i tornarà a carregar automàticament la interfície d'usuari durant el procés de restauració.",
|
||||
"RestartReloadNote": "Nota: {appName} es reiniciarà i tornarà a carregar automàticament la interfície d'usuari durant el procés de restauració.",
|
||||
"Series": "Sèries",
|
||||
"ShownAboveEachColumnWhenWeekIsTheActiveView": "Es mostra a sobre de cada columna quan la setmana és la visualització activa",
|
||||
"SorryThatAuthorCannotBeFound": "Ho sentim, aquesta pel·lícula no s'ha trobat.",
|
||||
@@ -500,27 +500,25 @@
|
||||
"ThisWillApplyToAllIndexersPleaseFollowTheRulesSetForthByThem": "Això s'aplicarà a tots els indexadors, si us plau, seguiu les regles establertes per ells",
|
||||
"UnableToLoadHistory": "No es pot carregar l'historial",
|
||||
"IconTooltip": "Programat",
|
||||
"ReadarrTags": "Etiquetes de Radarr",
|
||||
"ReadarrTags": "Etiquetes de {appName}",
|
||||
"DownloadPropersAndRepacksHelpTexts1": "Si s'ha d'actualitzar automàticament o no a Propers/Repacks",
|
||||
"GrabReleaseMessageText": "Lidarr no ha pogut determinar per a quina pel·lícula era aquest llançament. És possible que Lidarr no pugui importar automàticament aquesta versió. Voleu capturar \"{0}\"?",
|
||||
"GrabReleaseMessageText": "{appName} no ha pogut determinar per a quina pel·lícula era aquest llançament. És possible que {appName} no pugui importar automàticament aquesta versió. Voleu capturar \"{0}\"?",
|
||||
"IsCutoffCutoff": "Requisit",
|
||||
"MountCheckMessage": "El muntatge que conté una ruta de pel·lícula es munta com a només de lectura: ",
|
||||
"APIKey": "Clau API",
|
||||
"ApiKeyHelpTextWarning": "Cal reiniciar perquè tingui efecte",
|
||||
"RescanAfterRefreshHelpTextWarning": "Radarr no detectarà automàticament els canvis als fitxers quan no estigui configurat com a \"Sempre\"",
|
||||
"RescanAfterRefreshHelpTextWarning": "{appName} no detectarà automàticament els canvis als fitxers quan no estigui configurat com a \"Sempre\"",
|
||||
"ShowUnknownAuthorItems": "Mostra elements de pel·lícula desconeguda",
|
||||
"Size": " Mida",
|
||||
"SkipFreeSpaceCheckWhenImportingHelpText": "Utilitzeu-lo quan Lidarr no pugui detectar espai lliure a la carpeta arrel de la pel·lícula",
|
||||
"SkipFreeSpaceCheckWhenImportingHelpText": "Utilitzeu-lo quan {appName} no pugui detectar espai lliure a la carpeta arrel de la pel·lícula",
|
||||
"StandardBookFormat": "Format de pel·lícula estàndard",
|
||||
"UnableToAddANewImportListExclusionPleaseTryAgain": "No es pot afegir una nova llista d'exclusió, torneu-ho a provar.",
|
||||
"UnableToLoadReleaseProfiles": "No es poden carregar els perfils de retard",
|
||||
"UnmonitoredHelpText": "Inclou pel·lícules no monitorades al canal iCal",
|
||||
"UpdateAll": "Actualitzar-ho tot",
|
||||
"AutoUnmonitorPreviouslyDownloadedBooksHelpText": "Les pel·lícules suprimides del disc no es descarten automàticament al Radarr",
|
||||
"AutoUnmonitorPreviouslyDownloadedBooksHelpText": "Les pel·lícules suprimides del disc no es descarten automàticament al {appName}",
|
||||
"ChownGroupHelpText": "Nom del grup o gid. Utilitzeu gid per a sistemes de fitxers remots.",
|
||||
"AuthorClickToChangeBook": "Feu clic per canviar la pel·lícula",
|
||||
"ChmodFolderHelpTextWarning": "Això només funciona si l'usuari que executa Radarr és el propietari del fitxer. És millor assegurar-se que el client de descàrrega estableixi correctament els permisos.",
|
||||
"CopyUsingHardlinksHelpTextWarning": "De tant en tant, els bloquejos de fitxers poden impedir reanomenar els fitxers que s'estan sembrant. Podeu desactivar temporalment la compartició i utilitzar la funció de reanomenar de Radarr com a solució.",
|
||||
"ChmodFolderHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega estableixi correctament els permisos.",
|
||||
"CopyUsingHardlinksHelpTextWarning": "De tant en tant, els bloquejos de fitxers poden impedir reanomenar els fitxers que s'estan sembrant. Podeu desactivar temporalment la compartició i utilitzar la funció de reanomenar de {appName} com a solució.",
|
||||
"CouldntFindAnyResultsForTerm": "No s'ha pogut trobar cap resultat per a '{0}'",
|
||||
"CreateEmptyAuthorFolders": "Creeu carpetes buides per a les pel·lícules",
|
||||
"CreateEmptyAuthorFoldersHelpText": "Creeu carpetes de pel·lícules que falten durant l'exploració del disc",
|
||||
@@ -538,15 +536,15 @@
|
||||
"MetadataProfiles": "perfil de metadades",
|
||||
"OnBookFileDelete": "Al suprimir fitxer de pel·lícula",
|
||||
"OnBookFileDeleteForUpgrade": "Al suprimir el fitxer de pel·lícula per a l'actualització",
|
||||
"ReadarrSupportsAnyDownloadClient": "Radarr admet molts clients de baixada de torrent i usenet populars.",
|
||||
"ReadarrSupportsAnyIndexerThatUsesTheNewznabStandardAsWellAsOtherIndexersListedBelow": "Radarr admet qualsevol indexador que utilitzi l'estàndard Newznab, així com altres indexadors que s'enumeren a continuació.",
|
||||
"ReadarrSupportsAnyDownloadClient": "{appName} admet molts clients de baixada de torrent i usenet populars.",
|
||||
"ReadarrSupportsAnyIndexerThatUsesTheNewznabStandardAsWellAsOtherIndexersListedBelow": "{appName} admet qualsevol indexador que utilitzi l'estàndard Newznab, així com altres indexadors que s'enumeren a continuació.",
|
||||
"RecycleBinHelpText": "Els fitxers de pel·lícula aniran aquí quan se suprimeixin en lloc de suprimir-se permanentment",
|
||||
"RenameBooksHelpText": "Radarr utilitzarà el nom del fitxer existent si el reanomenat està desactivat",
|
||||
"RenameBooksHelpText": "{appName} utilitzarà el nom del fitxer existent si el reanomenat està desactivat",
|
||||
"RequiredHelpText": "El llançament ha de contenir almenys un d'aquests termes (no distingeix entre majúscules i minúscules)",
|
||||
"UILanguageHelpText": "Idioma que utilitzarà Radarr per a la interfície d'usuari",
|
||||
"UILanguageHelpText": "Idioma que utilitzarà {appName} per a la interfície d'usuari",
|
||||
"UnableToAddANewRootFolderPleaseTryAgain": "No es pot afegir un indexador nou, torneu-ho a provar.",
|
||||
"UnableToLoadMetadataProfiles": "No es poden carregar els perfils de qualitat",
|
||||
"UpdateMechanismHelpText": "Utilitzeu l'actualitzador integrat de Radarr o un script",
|
||||
"UpdateMechanismHelpText": "Utilitzeu l'actualitzador integrat de {appName} o un script",
|
||||
"UpdateSelected": "Actualització seleccionada",
|
||||
"Database": "Base de dades",
|
||||
"DeleteQualityProfileMessageText": "Esteu segur que voleu suprimir el perfil de qualitat '{name}'?",
|
||||
@@ -555,22 +553,22 @@
|
||||
"DeleteRootFolderMessageText": "Esteu segur que voleu suprimir l'indexador '{0}'?",
|
||||
"DeleteSelectedBookFiles": "Suprimeix els fitxers de pel·lícules seleccionats",
|
||||
"DeleteSelectedBookFilesMessageText": "Esteu segur que voleu suprimir els fitxers de pel·lícules seleccionats?",
|
||||
"IncludeUnknownAuthorItemsHelpText": "Mostra elements sense pel·lícula a la cua. Això podria incloure pel·lícules eliminades o qualsevol altra cosa de la categoria de Lidarr",
|
||||
"IncludeUnknownAuthorItemsHelpText": "Mostra elements sense pel·lícula a la cua. Això podria incloure pel·lícules eliminades o qualsevol altra cosa de la categoria de {appName}",
|
||||
"LogLevelvalueTraceTraceLoggingShouldOnlyBeEnabledTemporarily": "El registre de traça només s'hauria d'habilitar temporalment",
|
||||
"PortHelpTextWarning": "Cal reiniciar perquè tingui efecte",
|
||||
"RemotePathMappingCheckImportFailed": "Radarr no ha pogut importar una pel·lícula. Comproveu els vostres registres per obtenir més informació.",
|
||||
"RemotePathMappingCheckImportFailed": "{appName} no ha pogut importar una pel·lícula. Comproveu els vostres registres per obtenir més informació.",
|
||||
"RemoveTagExistingTag": "Etiqueta existent",
|
||||
"RemoveTagRemovingTag": "S'està eliminant l'etiqueta",
|
||||
"SupportsSearchvalueSearchIsNotSupportedWithThisIndexer": "La cerca no és compatible amb aquest indexador",
|
||||
"UnableToAddANewMetadataProfilePleaseTryAgain": "No es pot afegir un perfil de qualitat nou, torneu-ho a provar.",
|
||||
"RequiredPlaceHolder": "Afegeix una nova restricció",
|
||||
"20MinutesTwenty": "60 minuts: {0}",
|
||||
"20MinutesTwenty": "20 minuts: {0}",
|
||||
"AlternateTitles": "Títols alternatius",
|
||||
"AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de Radarr. Això inclou informació sobre el vostre navegador, quines pàgines Radarr WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió del temps d'execució. Utilitzarem aquesta informació per prioritzar les funcions i les correccions d'errors.",
|
||||
"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ó del temps d'execució. Utilitzarem aquesta informació per prioritzar les funcions i les correccions d'errors.",
|
||||
"AnalyticsEnabledHelpTextWarning": "Cal reiniciar perquè tingui efecte",
|
||||
"AuthenticationMethodHelpText": "Requereix nom d'usuari i contrasenya per accedir al radar",
|
||||
"CalendarWeekColumnHeaderHelpText": "Es mostra a sobre de cada columna quan la setmana és la visualització activa",
|
||||
"45MinutesFourtyFive": "60 minuts: {0}",
|
||||
"45MinutesFourtyFive": "45 minuts: {0}",
|
||||
"60MinutesSixty": "60 minuts: {0}",
|
||||
"BindAddressHelpTextWarning": "Cal reiniciar perquè tingui efecte",
|
||||
"BookIsDownloading": "La pel·lícula s'està baixant",
|
||||
@@ -586,7 +584,7 @@
|
||||
"BypassIfHighestQuality": "Bypass si és de màxima qualitat",
|
||||
"MinimumCustomFormatScore": "Puntuació mínima de format personalitzat",
|
||||
"CustomFormatScore": "Puntuació de format personalitzat",
|
||||
"EnableRssHelpText": "S'utilitzarà quan Radarr cerqui publicacions periòdicament mitjançant RSS Sync",
|
||||
"EnableRssHelpText": "S'utilitzarà quan {appName} cerqui publicacions periòdicament mitjançant RSS Sync",
|
||||
"ImportListMultipleMissingRoots": "Falten diverses carpetes arrel per a les llistes d'importació: {0}",
|
||||
"IndexerDownloadClientHelpText": "Especifiqueu quin client de baixada s'utilitza per a capturar des d'aquest indexador",
|
||||
"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",
|
||||
@@ -609,7 +607,7 @@
|
||||
"CustomFormat": "Format personalitzat",
|
||||
"CustomFormatSettings": "Configuració de formats personalitzats",
|
||||
"CustomFormats": "Formats personalitzats",
|
||||
"CutoffFormatScoreHelpText": "Un cop s'arribi a aquesta puntuació de format personalitzat, Radarr ja no baixarà pel·lícules",
|
||||
"CutoffFormatScoreHelpText": "Un cop s'arribi a aquesta puntuació de format personalitzat, {appName} ja no baixarà pel·lícules",
|
||||
"DeleteCustomFormatMessageText": "Esteu segur que voleu suprimir l'indexador '{0}'?",
|
||||
"ImportListMissingRoot": "Falta la carpeta arrel per a les llistes d'importació: {0}",
|
||||
"IndexerTagsHelpText": "Utilitzeu aquest indexador només per a pel·lícules amb almenys una etiqueta coincident. Deixeu-ho en blanc per utilitzar-ho amb totes les pel·lícules.",
|
||||
@@ -658,11 +656,11 @@
|
||||
"Activity": "Activitat",
|
||||
"AddNew": "Afegeix nou",
|
||||
"ApplyTagsHelpTextReplace": "Substitució: substituïu les etiquetes per les etiquetes introduïdes (no introduïu cap etiqueta per a esborrar totes les etiquetes)",
|
||||
"ApplyTagsHelpTextRemove": "Eliminar: elimina les etiquetes introduïdes",
|
||||
"ApplyTagsHelpTextRemove": "Eliminació: elimina les etiquetes introduïdes",
|
||||
"BlocklistReleases": "Llista de llançaments bloquejats",
|
||||
"AutoAdd": "Afegeix automàticament",
|
||||
"Backup": "Còpia de seguretat",
|
||||
"ApplyTagsHelpTextAdd": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent",
|
||||
"ApplyTagsHelpTextAdd": "Afegiment: afegeix les etiquetes a la llista d'etiquetes existent",
|
||||
"DeleteSelectedIndexersMessageText": "Esteu segur que voleu suprimir {count} indexador(s) seleccionat(s)?",
|
||||
"DeleteSelectedIndexers": "Suprimeix l'indexador(s)",
|
||||
"DeleteRemotePathMappingMessageText": "Esteu segur que voleu suprimir aquesta assignació de camins remots?",
|
||||
@@ -712,5 +710,91 @@
|
||||
"DownloadClientDelugeSettingsDirectory": "Directori de baixada",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Directori al qual es mou quan s'hagi completat",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge"
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"WhatsNew": "Novetats",
|
||||
"SelectDropdown": "Seleccioneu...",
|
||||
"NoCutoffUnmetItems": "No hi ha elements de tall no assolits",
|
||||
"ApplyTagsHelpTextHowToApplyAuthors": "Com aplicar etiquetes a les pel·lícules seleccionades",
|
||||
"DeleteConditionMessageText": "Esteu segur que voleu suprimir la condició '{name}'?",
|
||||
"NoChange": "Cap canvi",
|
||||
"SetTags": "Estableix etiquetes",
|
||||
"NoResultsFound": "Sense resultats",
|
||||
"Author": "Autor",
|
||||
"ResetQualityDefinitions": "Restableix les definicions de qualitat",
|
||||
"Small": "Petita",
|
||||
"TotalSpace": "Espai total",
|
||||
"BlocklistReleaseHelpText": "Impedeix que {appName} torni a capturar aquesta versió automàticament",
|
||||
"CatalogNumber": "número de catàleg",
|
||||
"LastWriteTime": "La darrera hora d'escriptura",
|
||||
"NextExecution": "Propera execució",
|
||||
"RemoveCompleted": "S'ha eliminat",
|
||||
"SelectReleaseGroup": "Seleccioneu grup de llançament",
|
||||
"CountDownloadClientsSelected": "{count} client(s) de baixada seleccionat(s)",
|
||||
"Authors": "Autor",
|
||||
"FreeSpace": "Espai lliure",
|
||||
"ExtraFileExtensionsHelpText": "Llista separada per comes de fitxers addicionals per importar (.nfo s'importarà com a .nfo-orig)",
|
||||
"BypassIfAboveCustomFormatScore": "Ometre si està per sobre de la puntuació de format personalitzada",
|
||||
"BypassIfAboveCustomFormatScoreHelpText": "Habiliteu l'omissió quan la versió tingui una puntuació superior a la puntuació mínima per al format personalitzat",
|
||||
"RedownloadFailed": "Tornar a baixar les baixades fallades",
|
||||
"ExistingTag": "Etiqueta existent",
|
||||
"RemoveFailed": "Ha fallat l'eliminació",
|
||||
"ImportLists": "llista d'importació",
|
||||
"RemovingTag": "S'està eliminant l'etiqueta",
|
||||
"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ó",
|
||||
"ExtraFileExtensionsHelpTextsExamples": "Exemples: '.sub, .nfo' o 'sub,nfo'",
|
||||
"SourceTitle": "Títol de la font",
|
||||
"NoEventsFound": "No s'han trobat esdeveniments",
|
||||
"InteractiveSearchModalHeader": "Cerca interactiva",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Llistes no disponibles a causa d'errors: {0}",
|
||||
"Medium": "Suport",
|
||||
"RecentChanges": "Canvis recents",
|
||||
"Rejections": "Rebutjats",
|
||||
"StatusEndedContinuing": "Continua",
|
||||
"DeleteBookFileMessageText": "Esteu segur que voleu suprimir '{path}'?",
|
||||
"DownloadClientTagHelpText": "Utilitzeu aquest indexador només per a pel·lícules amb almenys una etiqueta coincident. Deixeu-ho en blanc per utilitzar-ho amb totes les pel·lícules.",
|
||||
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "El client de baixada {downloadClientName} està configurat per eliminar les baixades completades. Això pot provocar que les baixades s'eliminin del vostre client abans que {1} pugui importar-les.",
|
||||
"AutoRedownloadFailedFromInteractiveSearchHelpText": "Cerqueu i intenteu baixar automàticament una versió diferent quan es trobi una versió fallida a la cerca interactiva",
|
||||
"FailedLoadingSearchResults": "No s'han pogut carregar els resultats de la cerca, torneu-ho a provar.",
|
||||
"IndexerFlags": "Indicadors de l'indexador",
|
||||
"Large": "Gran",
|
||||
"LastDuration": "Darrera durada",
|
||||
"LastExecution": "Darrere execució",
|
||||
"Library": "Biblioteca",
|
||||
"ListsSettingsSummary": "llista d'importació",
|
||||
"Loading": "Carregant",
|
||||
"MinimumCustomFormatScoreHelpText": "Puntuació mínima de format personalitzada necessaria per a evitar el retard del protocol preferit",
|
||||
"ProfilesSettingsSummary": "Perfils de qualitat, idioma, retard i llançament",
|
||||
"RemoveDownloadsAlert": "La configuració d'eliminació s'ha mogut a la configuració del client de baixada a la taula anterior.",
|
||||
"RemoveQueueItemConfirmation": "Esteu segur que voleu eliminar '{sourceTitle}' de la cua?",
|
||||
"RemoveSelectedItem": "Elimina l'element seleccionat",
|
||||
"RemoveSelectedItems": "Elimina els elements seleccionats",
|
||||
"RemoveSelectedItemsQueueMessageText": "Esteu segur que voleu eliminar {0} de la cua?",
|
||||
"SelectQuality": "Seleccioneu Qualitat",
|
||||
"SomeResultsAreHiddenByTheAppliedFilter": "Alguns resultats estan ocults pel filtre aplicat",
|
||||
"AuthBasic": "Basic (finestra emergent del navegador)",
|
||||
"AuthForm": "Formularis (pàgina d'inici de sessió)",
|
||||
"AuthenticationMethod": "Mètode d'autenticació",
|
||||
"AuthenticationMethodHelpTextWarning": "Seleccioneu un mètode d'autenticació vàlid",
|
||||
"AuthenticationRequired": "Autenticació necessària",
|
||||
"AuthenticationRequiredHelpText": "Canvia per a quines sol·licituds cal autenticar. No canvieu tret que entengueu els riscos.",
|
||||
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmeu la nova contrasenya",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "Introduïu una contrasenya nova",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Introduïu un nom d'usuari nou",
|
||||
"AuthenticationRequiredWarning": "Per a evitar l'accés remot sense autenticació, ara {appName} requereix que l'autenticació estigui activada. Opcionalment, podeu desactivar l'autenticació des d'adreces locals.",
|
||||
"DisabledForLocalAddresses": "Desactivat per a adreces locals",
|
||||
"Enabled": "Habilitat",
|
||||
"External": "Extern",
|
||||
"ApiKey": "Clau API",
|
||||
"FailedToFetchUpdates": "No s'han pogut obtenir les actualitzacions",
|
||||
"AptUpdater": "Utilitzeu apt per a instal·lar l'actualització",
|
||||
"BuiltIn": "Integrat",
|
||||
"CurrentlyInstalled": "Instal·lat actualment",
|
||||
"DockerUpdater": "actualitzeu el contenidor Docker per a rebre l'actualització",
|
||||
"ExternalUpdater": "{appName} està configurat per a utilitzar un mecanisme d'actualització extern",
|
||||
"InstallLatest": "Instal·la l'últim",
|
||||
"OnLatestVersion": "La darrera versió de {appName} ja està instal·lada",
|
||||
"Script": "Script",
|
||||
"UnmappedFiles": "Carpetes sense mapejar",
|
||||
"UpdateAppDirectlyLoadError": "No es pot actualitzar {appName} directament,",
|
||||
"AddMissing": "Afegeix faltants"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user