mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 200ef600cd | |||
| ad6228983b | |||
| 582ec9f7ce | |||
| 525e855038 | |||
| 7a629ed044 | |||
| 7f501322dd | |||
| 18bca0b228 | |||
| 9ddac60b47 | |||
| bd8bc0b35b | |||
| ae623f4481 | |||
| e67d133bb6 | |||
| 772ea95ce4 | |||
| 5459a7bb7e | |||
| 614f98f9b4 | |||
| 55763dae43 | |||
| a362dab503 | |||
| dba9fbf254 | |||
| 59a7605385 | |||
| f819e582cf | |||
| 0972d41bf8 | |||
| 05d2335bfe | |||
| 5b4f54a959 | |||
| bb5ad605fd | |||
| 52c3a95e63 | |||
| 52c5460537 | |||
| 280cec3d0e | |||
| f10c2c01d8 | |||
| 2b6a328dac | |||
| 4078525f67 | |||
| 214e4270ac | |||
| 5396dd3e8e | |||
| d5d4996c40 | |||
| 8b2223a9c4 | |||
| 2a3e7f8dae | |||
| 6406ed6289 | |||
| d5b0831b0f | |||
| 8d72d5dbab | |||
| 74d1ab84e2 | |||
| 7341d20c51 | |||
| 5abf4f2992 | |||
| cc90050c77 | |||
| 7ba8f8baee | |||
| 70aec175ef | |||
| 080dd301f3 | |||
| bab45481db | |||
| 3e1e03e0ce | |||
| bb599d6dc9 | |||
| c94685842f | |||
| 5966f6da51 | |||
| 229e0dfe5d | |||
| 5173daa265 | |||
| c2f770f242 | |||
| 0b7ce67635 | |||
| bc74456944 | |||
| fa460567a7 | |||
| 7dfceb307b | |||
| 305ad235a5 | |||
| 74c20e41bf | |||
| 347289b173 | |||
| 0ef3d2a5cc | |||
| e5519d60c9 | |||
| 3a85b3a060 | |||
| c1cdf44322 | |||
| f861e54139 | |||
| 279e1029e0 | |||
| b9ed39175b | |||
| faba3ada95 | |||
| e8647aee05 | |||
| eaf5ce52bc | |||
| 73ab2760e4 | |||
| 3bb036e8c6 | |||
| 6e05456d6a | |||
| 8563a42822 | |||
| 841d38f4a5 | |||
| 9326d88eb6 | |||
| 015da61004 | |||
| d02ea4b121 | |||
| 7bc9d700f9 | |||
| 661d72ef9b | |||
| 258a8d1c95 | |||
| d4459b9475 | |||
| a550c6554f | |||
| c1b26eec8d | |||
| ffe5ede55d | |||
| 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 |
@@ -120,6 +120,7 @@ _artifacts
|
|||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
|
_temp*
|
||||||
*.Result.xml
|
*.Result.xml
|
||||||
coverage*.xml
|
coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
|
|||||||
+11
-11
@@ -9,18 +9,18 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '0.4.0'
|
majorVersion: '0.4.17'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.424'
|
dotnetVersion: '6.0.427'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-22.04'
|
||||||
macImage: 'macOS-12'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -1102,19 +1102,19 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'readarr'
|
organization: 'readarr'
|
||||||
scannerMode: 'CLI'
|
scannerMode: 'cli'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'readarrui'
|
cliProjectKey: 'readarrui'
|
||||||
cliProjectName: 'ReadarrUI'
|
cliProjectName: 'ReadarrUI'
|
||||||
cliProjectVersion: '$(readarrVersion)'
|
cliProjectVersion: '$(readarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
@@ -1190,12 +1190,12 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'readarr'
|
organization: 'readarr'
|
||||||
scannerMode: 'MSBuild'
|
scannerMode: 'dotnet'
|
||||||
projectKey: 'Readarr_Readarr'
|
projectKey: 'Readarr_Readarr'
|
||||||
projectName: 'Readarr'
|
projectName: 'Readarr'
|
||||||
projectVersion: '$(readarrVersion)'
|
projectVersion: '$(readarrVersion)'
|
||||||
@@ -1208,10 +1208,10 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5
|
- task: reportgenerator@5.3.11
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
|
|||||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ module.exports = (env) => {
|
|||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
|
target: 'web',
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
@@ -67,7 +68,7 @@ module.exports = (env) => {
|
|||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: '[name]-[contenthash].js',
|
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ module.exports = (env) => {
|
|||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
@@ -181,7 +182,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.39'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -202,7 +203,7 @@ module.exports = (env) => {
|
|||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ function HistoryDetails(props) {
|
|||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message
|
message,
|
||||||
|
indexer
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,11 +178,21 @@ function HistoryDetails(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!message &&
|
indexer ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Indexer')}
|
||||||
|
data={indexer}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('Message')}
|
title={translate('Message')}
|
||||||
data={message}
|
data={message}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import Updates from 'System/Updates/Updates';
|
||||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
@@ -247,7 +247,7 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={UpdatesConnector}
|
component={Updates}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import AuthorsAppState from './AuthorsAppState';
|
import AuthorsAppState from './AuthorsAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
|
import SystemAppState from './SystemAppState';
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
|
|
||||||
interface FilterBuilderPropOption {
|
interface FilterBuilderPropOption {
|
||||||
@@ -35,10 +36,24 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
authors: AuthorsAppState;
|
authors: AuthorsAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
|
system: SystemAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
@@ -7,13 +8,16 @@ import ImportList from 'typings/ImportList';
|
|||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
import IndexerFlag from 'typings/IndexerFlag';
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
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
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export type GeneralAppState = AppSectionItemState<General>;
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -33,11 +37,12 @@ export type UiSettingsAppState = AppSectionState<UiSettings>;
|
|||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
general: GeneralAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
uiSettings: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsAppState;
|
export default SettingsAppState;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export type AuthorStatus = 'continuing' | 'ended';
|
||||||
|
|
||||||
interface Author extends ModelBase {
|
interface Author extends ModelBase {
|
||||||
added: string;
|
added: string;
|
||||||
genres: string[];
|
genres: string[];
|
||||||
@@ -10,6 +12,7 @@ interface Author extends ModelBase {
|
|||||||
metadataProfileId: number;
|
metadataProfileId: number;
|
||||||
rootFolderPath: string;
|
rootFolderPath: string;
|
||||||
sortName: string;
|
sortName: string;
|
||||||
|
status: AuthorStatus;
|
||||||
tags: number[];
|
tags: number[];
|
||||||
authorName: string;
|
authorName: string;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { AuthorStatus } from 'Author/Author';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
export function getAuthorStatusDetails(status: AuthorStatus) {
|
||||||
|
let statusDetails = {
|
||||||
|
icon: icons.AUTHOR_CONTINUING,
|
||||||
|
title: translate('StatusEndedContinuing'),
|
||||||
|
message: translate('ContinuingMoreBooksAreExpected'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === 'ended') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.AUTHOR_ENDED,
|
||||||
|
title: translate('StatusEndedEnded'),
|
||||||
|
message: translate('ContinuingNoAdditionalBooksAreExpected'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusDetails;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
|||||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -17,7 +18,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||||
@@ -412,22 +413,25 @@ class AuthorDetails extends Component {
|
|||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !booksError && !bookFilesError &&
|
!isPopulated && !booksError && !bookFilesError ?
|
||||||
<LoadingIndicator />
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && booksError &&
|
!isFetching && booksError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingBooksFailed')}
|
{translate('LoadingBooksFailed')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && bookFilesError &&
|
!isFetching && bookFilesError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingBookFilesFailed')}
|
{translate('LoadingBookFilesFailed')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
import AuthorPoster from 'Author/AuthorPoster';
|
import AuthorPoster from 'Author/AuthorPoster';
|
||||||
|
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
@@ -11,7 +12,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||||
import fonts from 'Styles/Variables/fonts';
|
import fonts from 'Styles/Variables/fonts';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import stripHtml from 'Utilities/String/stripHtml';
|
import stripHtml from 'Utilities/String/stripHtml';
|
||||||
@@ -87,11 +88,11 @@ class AuthorDetailsHeader extends Component {
|
|||||||
titleWidth
|
titleWidth
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const statusDetails = getAuthorStatusDetails(status);
|
||||||
|
|
||||||
const fanartUrl = getFanartUrl(images);
|
const fanartUrl = getFanartUrl(images);
|
||||||
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
||||||
|
|
||||||
const continuing = status === 'continuing';
|
|
||||||
|
|
||||||
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
||||||
|
|
||||||
if (bookFileCount === 1) {
|
if (bookFileCount === 1) {
|
||||||
@@ -213,7 +214,7 @@ class AuthorDetailsHeader extends Component {
|
|||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{
|
{
|
||||||
<QualityProfileNameConnector
|
<QualityProfileName
|
||||||
qualityProfileId={qualityProfileId}
|
qualityProfileId={qualityProfileId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -236,16 +237,16 @@ class AuthorDetailsHeader extends Component {
|
|||||||
|
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
|
title={statusDetails.message}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
name={statusDetails.icon}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{continuing ? 'Continuing' : 'Deceased'}
|
{statusDetails.title}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
|||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
||||||
import styles from './AuthorIndexOverviewInfo.css';
|
import styles from './AuthorIndexOverviewInfo.css';
|
||||||
|
|
||||||
@@ -76,9 +77,9 @@ function getInfoRowProps(row, props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||||
return {
|
return {
|
||||||
title: 'Quality Profile',
|
title: translate('QualityProfile'),
|
||||||
iconName: icons.PROFILE,
|
iconName: icons.PROFILE,
|
||||||
label: props.qualityProfile.name
|
label: props.qualityProfile.name
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -235,12 +235,12 @@ class AuthorIndexPoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{showQualityProfile && !!qualityProfile?.name ? (
|
||||||
showQualityProfile &&
|
<div className={styles.title} title={translate('QualityProfile')}>
|
||||||
<div className={styles.title}>
|
{qualityProfile.name}
|
||||||
{qualityProfile.name}
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
}
|
|
||||||
{
|
{
|
||||||
nextAiring &&
|
nextAiring &&
|
||||||
<div className={styles.nextAiring}>
|
<div className={styles.nextAiring}>
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ class AuthorIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{qualityProfile.name}
|
{qualityProfile?.name ?? ''}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ class AuthorIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{metadataProfile.name}
|
{metadataProfile?.name ?? ''}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -15,6 +16,8 @@ function AuthorStatusCell(props) {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const statusDetails = getAuthorStatusDetails(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={className}
|
className={className}
|
||||||
@@ -28,8 +31,8 @@ function AuthorStatusCell(props) {
|
|||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
name={statusDetails.icon}
|
||||||
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
|
title={`${statusDetails.title}: ${statusDetails.message}`}
|
||||||
/>
|
/>
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './NoAuthor.css';
|
import styles from './NoAuthor.css';
|
||||||
|
|
||||||
function NoAuthor(props) {
|
function NoAuthor(props) {
|
||||||
@@ -31,7 +32,7 @@ function NoAuthor(props) {
|
|||||||
to="/settings/mediamanagement"
|
to="/settings/mediamanagement"
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
>
|
>
|
||||||
Add Root Folder
|
{translate('AddRootFolder')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ function NoAuthor(props) {
|
|||||||
to="/add/search"
|
to="/add/search"
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
>
|
>
|
||||||
Add New Author
|
{translate('AddNewAuthor')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
|||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
||||||
import styles from './BookIndexOverviewInfo.css';
|
import styles from './BookIndexOverviewInfo.css';
|
||||||
|
|
||||||
@@ -71,9 +72,9 @@ function getInfoRowProps(row, props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||||
return {
|
return {
|
||||||
title: 'Quality Profile',
|
title: translate('QualityProfile'),
|
||||||
iconName: icons.PROFILE,
|
iconName: icons.PROFILE,
|
||||||
label: props.qualityProfile.name
|
label: props.qualityProfile.name
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{showQualityProfile && !!qualityProfile?.name ? (
|
||||||
showQualityProfile &&
|
<div className={styles.title} title={translate('QualityProfile')}>
|
||||||
<div className={styles.title}>
|
{qualityProfile.name}
|
||||||
{qualityProfile.name}
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
}
|
|
||||||
{
|
{
|
||||||
nextAiring &&
|
nextAiring &&
|
||||||
<div className={styles.nextAiring}>
|
<div className={styles.nextAiring}>
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{qualityProfile.name}
|
{qualityProfile?.name ?? ''}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||||
|
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import BookshelfBook from './BookshelfBook';
|
import BookshelfBook from './BookshelfBook';
|
||||||
import styles from './BookshelfRow.css';
|
import styles from './BookshelfRow.css';
|
||||||
|
|
||||||
@@ -30,6 +29,8 @@ class BookshelfRow extends Component {
|
|||||||
onBookMonitoredPress
|
onBookMonitoredPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const statusDetails = getAuthorStatusDetails(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VirtualTableSelectCell
|
<VirtualTableSelectCell
|
||||||
@@ -52,8 +53,8 @@ class BookshelfRow extends Component {
|
|||||||
<VirtualTableRowCell className={styles.status}>
|
<VirtualTableRowCell className={styles.status}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
name={statusDetails.icon}
|
||||||
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
|
title={statusDetails.title}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
|
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
}
|
}
|
||||||
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
|||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
if ((/^botton/).test(data.placement)) {
|
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||||
data.styles.maxHeight = windowHeight - bottom;
|
|
||||||
} else {
|
|
||||||
data.styles.maxHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
|
|||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true,
|
||||||
|
boundariesElement: 'viewport'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ function createMapStateToProps() {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: '',
|
value: translate('NoChange'),
|
||||||
name: translate('NoChange'),
|
|
||||||
isDisabled: includeNoChangeDisabled,
|
isDisabled: includeNoChangeDisabled,
|
||||||
isMissing: false
|
isMissing: false
|
||||||
});
|
});
|
||||||
@@ -39,7 +38,6 @@ function createMapStateToProps() {
|
|||||||
values.push({
|
values.push({
|
||||||
key: '',
|
key: '',
|
||||||
value: '',
|
value: '',
|
||||||
name: '',
|
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
isHidden: true
|
isHidden: true
|
||||||
});
|
});
|
||||||
@@ -56,8 +54,7 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
values.push({
|
values.push({
|
||||||
key: ADD_NEW_KEY,
|
key: ADD_NEW_KEY,
|
||||||
value: '',
|
value: 'Add a new path'
|
||||||
name: 'Add a new path'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -105,6 +102,27 @@ class RootFolderSelectInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.values === values) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
|
||||||
|
const defaultValue = values[0];
|
||||||
|
|
||||||
|
if (defaultValue.key !== ADD_NEW_KEY) {
|
||||||
|
onChange({ name, value: defaultValue.key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorFolder {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--disabledColor);
|
||||||
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
color: var(--darkGray);
|
color: var(--darkGray);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'authorFolder': string;
|
||||||
'freeSpace': string;
|
'freeSpace': string;
|
||||||
'isMissing': string;
|
'isMissing': string;
|
||||||
'isMobile': string;
|
'isMobile': string;
|
||||||
'optionText': string;
|
'optionText': string;
|
||||||
|
'value': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -7,18 +7,24 @@ import styles from './RootFolderSelectInputOption.css';
|
|||||||
|
|
||||||
function RootFolderSelectInputOption(props) {
|
function RootFolderSelectInputOption(props) {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
value,
|
value,
|
||||||
name,
|
name,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
|
authorFolder,
|
||||||
isMissing,
|
isMissing,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
isWindows,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const text = value === '' ? name : `${name} [${value}]`;
|
const slashCharacter = isWindows ? '\\' : '/';
|
||||||
|
|
||||||
|
const text = name === '' ? value : `[${name}] ${value}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputOption
|
<EnhancedSelectInputOption
|
||||||
|
id={id}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
@@ -27,7 +33,18 @@ function RootFolderSelectInputOption(props) {
|
|||||||
isMobile && styles.isMobile
|
isMobile && styles.isMobile
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>{text}</div>
|
<div className={styles.value}>
|
||||||
|
{text}
|
||||||
|
|
||||||
|
{
|
||||||
|
authorFolder && id !== 'addNew' ?
|
||||||
|
<div className={styles.authorFolder}>
|
||||||
|
{slashCharacter}
|
||||||
|
{authorFolder}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
freeSpace == null ?
|
freeSpace == null ?
|
||||||
@@ -50,11 +67,18 @@ function RootFolderSelectInputOption(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RootFolderSelectInputOption.propTypes = {
|
RootFolderSelectInputOption.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
|
authorFolder: PropTypes.string,
|
||||||
isMissing: PropTypes.bool,
|
isMissing: PropTypes.bool,
|
||||||
isMobile: PropTypes.bool.isRequired
|
isMobile: PropTypes.bool.isRequired,
|
||||||
|
isWindows: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
RootFolderSelectInputOption.defaultProps = {
|
||||||
|
name: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RootFolderSelectInputOption;
|
export default RootFolderSelectInputOption;
|
||||||
|
|||||||
@@ -7,12 +7,22 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path {
|
.pathContainer {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
|
display: flex;
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorFolder {
|
||||||
|
@add-mixin truncate;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
color: var(--disabledColor);
|
||||||
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'authorFolder': string;
|
||||||
'freeSpace': string;
|
'freeSpace': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
|
'pathContainer': string;
|
||||||
'selectedValue': string;
|
'selectedValue': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -9,19 +9,34 @@ function RootFolderSelectInputSelectedValue(props) {
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
|
authorFolder,
|
||||||
includeFreeSpace,
|
includeFreeSpace,
|
||||||
|
isWindows,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const text = value === '' ? name : `${name} [${value}]`;
|
const slashCharacter = isWindows ? '\\' : '/';
|
||||||
|
|
||||||
|
const text = name === '' ? value : `[${name}] ${value}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputSelectedValue
|
<EnhancedSelectInputSelectedValue
|
||||||
className={styles.selectedValue}
|
className={styles.selectedValue}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.path}>
|
<div className={styles.pathContainer}>
|
||||||
{text}
|
<div className={styles.path}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
authorFolder ?
|
||||||
|
<div className={styles.authorFolder}>
|
||||||
|
{slashCharacter}
|
||||||
|
{authorFolder}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -38,10 +53,13 @@ RootFolderSelectInputSelectedValue.propTypes = {
|
|||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
|
authorFolder: PropTypes.string,
|
||||||
|
isWindows: PropTypes.bool,
|
||||||
includeFreeSpace: PropTypes.bool.isRequired
|
includeFreeSpace: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
RootFolderSelectInputSelectedValue.defaultProps = {
|
RootFolderSelectInputSelectedValue.defaultProps = {
|
||||||
|
name: '',
|
||||||
includeFreeSpace: true
|
includeFreeSpace: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class SelectInput extends Component {
|
|||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
value: optionValue,
|
value: optionValue,
|
||||||
|
isDisabled: optionIsDisabled = false,
|
||||||
...otherOptionProps
|
...otherOptionProps
|
||||||
} = option;
|
} = option;
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ class SelectInput extends Component {
|
|||||||
<option
|
<option
|
||||||
key={key}
|
key={key}
|
||||||
value={key}
|
value={key}
|
||||||
|
disabled={optionIsDisabled}
|
||||||
{...otherOptionProps}
|
{...otherOptionProps}
|
||||||
>
|
>
|
||||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { align, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import Link from './Link';
|
||||||
|
import styles from './Button.css';
|
||||||
|
|
||||||
|
class Button extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
buttonGroupPosition,
|
||||||
|
kind,
|
||||||
|
size,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles[kind],
|
||||||
|
styles[size],
|
||||||
|
buttonGroupPosition && styles[buttonGroupPosition]
|
||||||
|
)}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
buttonGroupPosition: PropTypes.oneOf(align.all),
|
||||||
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
|
size: PropTypes.oneOf(sizes.all),
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.defaultProps = {
|
||||||
|
className: styles.button,
|
||||||
|
kind: kinds.DEFAULT,
|
||||||
|
size: sizes.MEDIUM
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import { align, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import Link, { LinkProps } from './Link';
|
|
||||||
import styles from './Button.css';
|
|
||||||
|
|
||||||
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
|
||||||
buttonGroupPosition?: Extract<
|
|
||||||
(typeof align.all)[number],
|
|
||||||
keyof typeof styles
|
|
||||||
>;
|
|
||||||
kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>;
|
|
||||||
size?: Extract<(typeof sizes.all)[number], keyof typeof styles>;
|
|
||||||
children: Required<LinkProps['children']>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Button({
|
|
||||||
className = styles.button,
|
|
||||||
buttonGroupPosition,
|
|
||||||
kind = kinds.DEFAULT,
|
|
||||||
size = sizes.MEDIUM,
|
|
||||||
...otherProps
|
|
||||||
}: ButtonProps) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[kind],
|
|
||||||
styles[size],
|
|
||||||
buttonGroupPosition && styles[buttonGroupPosition]
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -83,13 +83,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.modal.small,
|
|
||||||
.modal.medium {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
|
||||||
.modalContainer {
|
.modalContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
|
||||||
|
|
||||||
.isDisabled {
|
&.isDisabled {
|
||||||
color: var(--disabledColor);
|
color: var(--disabledColor);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
line-height: 1.52857143;
|
line-height: 1.52857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.pager {
|
.pager {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.headerCell {
|
.headerCell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,11 +35,12 @@
|
|||||||
.message {
|
.message {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: $largeFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpText {
|
.helpText {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: 300;
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ class AddNewItem extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
items
|
items,
|
||||||
|
hasExistingAuthors
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const term = this.state.term;
|
const term = this.state.term;
|
||||||
@@ -186,7 +187,8 @@ class AddNewItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!term &&
|
term ?
|
||||||
|
null :
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
|
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
|
||||||
@@ -199,6 +201,24 @@ class AddNewItem extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!term && !hasExistingAuthors ?
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.noAuthorsText}>
|
||||||
|
You haven't added any authors yet, do you want to add an existing library location (Root Folder) and update?
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
to="/settings/mediamanagement"
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
>
|
||||||
|
{translate('AddRootFolder')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
@@ -213,6 +233,7 @@ AddNewItem.propTypes = {
|
|||||||
isAdding: PropTypes.bool.isRequired,
|
isAdding: PropTypes.bool.isRequired,
|
||||||
addError: PropTypes.object,
|
addError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
hasExistingAuthors: PropTypes.bool.isRequired,
|
||||||
onSearchChange: PropTypes.func.isRequired,
|
onSearchChange: PropTypes.func.isRequired,
|
||||||
onClearSearch: PropTypes.func.isRequired
|
onClearSearch: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import AddNewItem from './AddNewItem';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.search,
|
(state) => state.search,
|
||||||
|
(state) => state.authors.items.length,
|
||||||
(state) => state.router.location,
|
(state) => state.router.location,
|
||||||
(search, location) => {
|
(search, existingAuthorsCount, location) => {
|
||||||
const { params } = parseUrl(location.search);
|
const { params } = parseUrl(location.search);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...search,
|
||||||
term: params.term,
|
term: params.term,
|
||||||
...search
|
hasExistingAuthors: existingAuthorsCount > 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
||||||
import styles from './AddNewAuthorModalContent.css';
|
import styles from './AddNewAuthorModalContent.css';
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ class AddNewAuthorModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Add new Author
|
{translate('AddNewAuthor')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -133,7 +134,7 @@ class AddNewAuthorModalContent extends Component {
|
|||||||
|
|
||||||
AddNewAuthorModalContent.propTypes = {
|
AddNewAuthorModalContent.propTypes = {
|
||||||
authorName: PropTypes.string.isRequired,
|
authorName: PropTypes.string.isRequired,
|
||||||
disambiguation: PropTypes.string.isRequired,
|
disambiguation: PropTypes.string,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isAdding: PropTypes.bool.isRequired,
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
|
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import AddNewAuthorModalContent from './AddNewAuthorModalContent';
|
import AddNewAuthorModalContent from './AddNewAuthorModalContent';
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ function createMapStateToProps() {
|
|||||||
(state) => state.search,
|
(state) => state.search,
|
||||||
(state) => state.settings.metadataProfiles,
|
(state) => state.settings.metadataProfiles,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(searchState, metadataProfiles, dimensions) => {
|
createSystemStatusSelector(),
|
||||||
|
(searchState, metadataProfiles, dimensions, systemStatus) => {
|
||||||
const {
|
const {
|
||||||
isAdding,
|
isAdding,
|
||||||
addError,
|
addError,
|
||||||
@@ -32,6 +34,7 @@ function createMapStateToProps() {
|
|||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
validationWarnings,
|
validationWarnings,
|
||||||
|
isWindows: systemStatus.isWindows,
|
||||||
...settings
|
...settings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class AddNewAuthorSearchResult extends Component {
|
|||||||
status,
|
status,
|
||||||
overview,
|
overview,
|
||||||
ratings,
|
ratings,
|
||||||
|
folder,
|
||||||
images,
|
images,
|
||||||
isExistingAuthor,
|
isExistingAuthor,
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
@@ -205,6 +206,7 @@ class AddNewAuthorSearchResult extends Component {
|
|||||||
disambiguation={disambiguation}
|
disambiguation={disambiguation}
|
||||||
year={year}
|
year={year}
|
||||||
overview={overview}
|
overview={overview}
|
||||||
|
folder={folder}
|
||||||
images={images}
|
images={images}
|
||||||
onModalClose={this.onAddAuthorModalClose}
|
onModalClose={this.onAddAuthorModalClose}
|
||||||
/>
|
/>
|
||||||
@@ -222,6 +224,7 @@ AddNewAuthorSearchResult.propTypes = {
|
|||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
|
folder: PropTypes.string.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isExistingAuthor: PropTypes.bool.isRequired,
|
isExistingAuthor: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired
|
isSmallScreen: PropTypes.bool.isRequired
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import stripHtml from 'Utilities/String/stripHtml';
|
import stripHtml from 'Utilities/String/stripHtml';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
|
||||||
import styles from './AddNewBookModalContent.css';
|
import styles from './AddNewBookModalContent.css';
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ class AddNewBookModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Add new Book
|
{translate('AddNewBook')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
|
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import AddNewBookModalContent from './AddNewBookModalContent';
|
import AddNewBookModalContent from './AddNewBookModalContent';
|
||||||
|
|
||||||
@@ -13,7 +14,8 @@ function createMapStateToProps() {
|
|||||||
(state) => state.search,
|
(state) => state.search,
|
||||||
(state) => state.settings.metadataProfiles,
|
(state) => state.settings.metadataProfiles,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(isExistingAuthor, searchState, metadataProfiles, dimensions) => {
|
createSystemStatusSelector(),
|
||||||
|
(isExistingAuthor, searchState, metadataProfiles, dimensions, systemStatus) => {
|
||||||
const {
|
const {
|
||||||
isAdding,
|
isAdding,
|
||||||
addError,
|
addError,
|
||||||
@@ -33,6 +35,7 @@ function createMapStateToProps() {
|
|||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
validationWarnings,
|
validationWarnings,
|
||||||
|
isWindows: systemStatus.isWindows,
|
||||||
...settings
|
...settings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ class AddNewBookSearchResult extends Component {
|
|||||||
disambiguation={disambiguation}
|
disambiguation={disambiguation}
|
||||||
authorName={author.authorName}
|
authorName={author.authorName}
|
||||||
overview={overview}
|
overview={overview}
|
||||||
|
folder={author.folder}
|
||||||
images={images}
|
images={images}
|
||||||
onModalClose={this.onAddBookModalClose}
|
onModalClose={this.onAddBookModalClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ class AddAuthorOptionsForm extends Component {
|
|||||||
includeNoneMetadataProfile,
|
includeNoneMetadataProfile,
|
||||||
includeSpecificBookMonitor,
|
includeSpecificBookMonitor,
|
||||||
showMetadataProfile,
|
showMetadataProfile,
|
||||||
|
folder,
|
||||||
tags,
|
tags,
|
||||||
|
isWindows,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -54,6 +56,15 @@ class AddAuthorOptionsForm extends Component {
|
|||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
name="rootFolderPath"
|
name="rootFolderPath"
|
||||||
|
valueOptions={{
|
||||||
|
authorFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
selectedValueOptions={{
|
||||||
|
authorFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
helpText={translate('AddNewAuthorRootFolderHelpText', { folder })}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...rootFolderPath}
|
{...rootFolderPath}
|
||||||
/>
|
/>
|
||||||
@@ -179,8 +190,14 @@ AddAuthorOptionsForm.propTypes = {
|
|||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
showMetadataProfile: PropTypes.bool.isRequired,
|
||||||
includeNoneMetadataProfile: PropTypes.bool.isRequired,
|
includeNoneMetadataProfile: PropTypes.bool.isRequired,
|
||||||
includeSpecificBookMonitor: PropTypes.bool.isRequired,
|
includeSpecificBookMonitor: PropTypes.bool.isRequired,
|
||||||
|
folder: PropTypes.string.isRequired,
|
||||||
tags: PropTypes.object.isRequired,
|
tags: PropTypes.object.isRequired,
|
||||||
|
isWindows: PropTypes.bool.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AddAuthorOptionsForm.defaultProps = {
|
||||||
|
includeSpecificBookMonitor: false
|
||||||
|
};
|
||||||
|
|
||||||
export default AddAuthorOptionsForm;
|
export default AddAuthorOptionsForm;
|
||||||
|
|||||||
+2
-4
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
|
||||||
import {
|
import {
|
||||||
bulkDeleteDownloadClients,
|
bulkDeleteDownloadClients,
|
||||||
bulkEditDownloadClients,
|
bulkEditDownloadClients,
|
||||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||||||
typeof ManageDownloadClientsModalRow
|
typeof ManageDownloadClientsModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
@@ -82,8 +82,6 @@ const COLUMNS = [
|
|||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
sortKey?: string;
|
|
||||||
sortDirection?: SortDirection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ function UpdateSettings(props) {
|
|||||||
const {
|
const {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
settings,
|
settings,
|
||||||
isWindows,
|
|
||||||
packageUpdateMechanism,
|
packageUpdateMechanism,
|
||||||
onInputChange
|
onInputChange
|
||||||
} = props;
|
} = props;
|
||||||
@@ -44,10 +43,10 @@ function UpdateSettings(props) {
|
|||||||
value: titleCase(packageUpdateMechanism)
|
value: titleCase(packageUpdateMechanism)
|
||||||
});
|
});
|
||||||
} else {
|
} 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 (
|
return (
|
||||||
<FieldSet legend={translate('Updates')}>
|
<FieldSet legend={translate('Updates')}>
|
||||||
@@ -60,8 +59,8 @@ function UpdateSettings(props) {
|
|||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AUTO_COMPLETE}
|
type={inputTypes.AUTO_COMPLETE}
|
||||||
name="branch"
|
name="branch"
|
||||||
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
|
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
||||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
|
helpLink="https://wiki.servarr.com/readarr/settings#updates"
|
||||||
{...branch}
|
{...branch}
|
||||||
values={branchValues}
|
values={branchValues}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
@@ -69,62 +68,59 @@ function UpdateSettings(props) {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
<div>
|
||||||
!isWindows &&
|
<FormGroup
|
||||||
<div>
|
advancedSettings={advancedSettings}
|
||||||
<FormGroup
|
isAdvanced={true}
|
||||||
advancedSettings={advancedSettings}
|
size={sizes.MEDIUM}
|
||||||
isAdvanced={true}
|
>
|
||||||
size={sizes.MEDIUM}
|
<FormLabel>{translate('Automatic')}</FormLabel>
|
||||||
>
|
|
||||||
<FormLabel>{translate('Automatic')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="updateAutomatically"
|
name="updateAutomatically"
|
||||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
|
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...updateAutomatically}
|
{...updateAutomatically}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</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
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
>
|
>
|
||||||
<FormLabel>{translate('Mechanism')}</FormLabel>
|
<FormLabel>{translate('ScriptPath')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.TEXT}
|
||||||
name="updateMechanism"
|
name="updateScriptPath"
|
||||||
values={updateOptions}
|
helpText={translate('UpdateScriptPathHelpText')}
|
||||||
helpText={translate('UpdateMechanismHelpText')}
|
|
||||||
helpLink="https://wiki.servarr.com/readarr/faq#how-do-i-update-my-readarr"
|
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...updateMechanism}
|
{...updateScriptPath}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
}
|
||||||
{
|
</div>
|
||||||
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>
|
|
||||||
}
|
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ function EditImportListExclusionModalContent(props) {
|
|||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('MusicbrainzId')}
|
{translate('ForeignId')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
|
||||||
import {
|
import {
|
||||||
bulkDeleteIndexers,
|
bulkDeleteIndexers,
|
||||||
bulkEditIndexers,
|
bulkEditIndexers,
|
||||||
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
|||||||
typeof ManageIndexersModalRow
|
typeof ManageIndexersModalRow
|
||||||
>['onSelectedChange'];
|
>['onSelectedChange'];
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: () => translate('Name'),
|
label: () => translate('Name'),
|
||||||
@@ -82,8 +82,6 @@ const COLUMNS = [
|
|||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
sortKey?: string;
|
|
||||||
sortDirection?: SortDirection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -15,11 +14,11 @@ function createMapStateToProps() {
|
|||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
(state) => state.settings.namingExamples,
|
(state) => state.settings.namingExamples,
|
||||||
createSettingsSectionSelector(SECTION),
|
createSettingsSectionSelector(SECTION),
|
||||||
(advancedSettings, examples, sectionSettings) => {
|
(advancedSettings, namingExamples, sectionSettings) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
examples: examples.item,
|
examples: namingExamples.item,
|
||||||
examplesPopulated: !_.isEmpty(examples.item),
|
examplesPopulated: namingExamples.isPopulated,
|
||||||
...sectionSettings
|
...sectionSettings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
interface QualityProfileNameProps {
|
||||||
|
qualityProfileId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityProfileName({ qualityProfileId }: QualityProfileNameProps) {
|
||||||
|
const qualityProfile = useSelector(
|
||||||
|
createQualityProfileSelectorForHook(qualityProfileId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return <span>{qualityProfile?.name ?? translate('Unknown')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityProfileName;
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createQualityProfileSelector(),
|
|
||||||
(qualityProfile) => {
|
|
||||||
return {
|
|
||||||
name: qualityProfile.name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function QualityProfileNameConnector({ name, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileNameConnector.propTypes = {
|
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(QualityProfileNameConnector);
|
|
||||||
@@ -45,6 +45,12 @@ export const defaultState = {
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'books.lastSearchTime',
|
||||||
|
label: 'Last Searched',
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
columnLabel: 'Actions',
|
columnLabel: 'Actions',
|
||||||
@@ -108,6 +114,12 @@ export const defaultState = {
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'books.lastSearchTime',
|
||||||
|
label: 'Last Searched',
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
columnLabel: 'Actions',
|
columnLabel: 'Actions',
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
@@ -27,6 +27,12 @@ export default function translate(
|
|||||||
key: string,
|
key: string,
|
||||||
tokens: Record<string, string | number | boolean> = {}
|
tokens: Record<string, string | number | boolean> = {}
|
||||||
) {
|
) {
|
||||||
|
const { isProduction = true } = window.Readarr;
|
||||||
|
|
||||||
|
if (!isProduction && !(key in translations)) {
|
||||||
|
console.warn(`Missing translation for key: ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
tokens.appName = 'Readarr';
|
tokens.appName = 'Readarr';
|
||||||
|
|||||||
@@ -131,13 +131,15 @@ class CutoffUnmetConnector extends Component {
|
|||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.BOOK_SEARCH,
|
name: commandNames.BOOK_SEARCH,
|
||||||
bookIds: selected
|
bookIds: selected,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllCutoffUnmetPress = () => {
|
onSearchAllCutoffUnmetPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH
|
name: commandNames.CUTOFF_UNMET_BOOK_SEARCH,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function CutoffUnmetRow(props) {
|
|||||||
releaseDate,
|
releaseDate,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
title,
|
title,
|
||||||
|
lastSearchTime,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
@@ -68,6 +69,15 @@ function CutoffUnmetRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'books.lastSearchTime') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={lastSearchTime}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'releaseDate') {
|
if (name === 'releaseDate') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
@@ -105,6 +115,7 @@ CutoffUnmetRow.propTypes = {
|
|||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
lastSearchTime: PropTypes.string,
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -121,13 +121,15 @@ class MissingConnector extends Component {
|
|||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.BOOK_SEARCH,
|
name: commandNames.BOOK_SEARCH,
|
||||||
bookIds: selected
|
bookIds: selected,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllMissingPress = () => {
|
onSearchAllMissingPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.MISSING_BOOK_SEARCH
|
name: commandNames.MISSING_BOOK_SEARCH,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function MissingRow(props) {
|
|||||||
releaseDate,
|
releaseDate,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
title,
|
title,
|
||||||
|
lastSearchTime,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
@@ -77,6 +78,15 @@ function MissingRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'books.lastSearchTime') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
date={lastSearchTime}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<BookSearchCellConnector
|
<BookSearchCellConnector
|
||||||
@@ -104,6 +114,7 @@ MissingRow.propTypes = {
|
|||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
lastSearchTime: PropTypes.string,
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -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;
|
showRelativeDates: boolean;
|
||||||
shortDateFormat: string;
|
shortDateFormat: string;
|
||||||
longDateFormat: string;
|
longDateFormat: string;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
Vendored
+1
@@ -7,5 +7,6 @@ interface Window {
|
|||||||
theme: string;
|
theme: string;
|
||||||
urlBase: string;
|
urlBase: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
isProduction: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-50
@@ -25,34 +25,33 @@
|
|||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "6.4.0",
|
"@fortawesome/fontawesome-free": "6.7.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
"@fortawesome/free-regular-svg-icons": "6.7.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.2",
|
||||||
"@microsoft/signalr": "6.0.25",
|
"@microsoft/signalr": "6.0.25",
|
||||||
"@sentry/browser": "7.51.2",
|
"@sentry/browser": "7.119.1",
|
||||||
"@sentry/integrations": "7.51.2",
|
"@sentry/integrations": "7.119.1",
|
||||||
"@types/node": "18.19.31",
|
"@types/node": "20.16.11",
|
||||||
"@types/react": "18.2.79",
|
"@types/react": "18.2.79",
|
||||||
"@types/react-dom": "18.2.25",
|
"@types/react-dom": "18.2.25",
|
||||||
"ansi-colors": "4.1.3",
|
"classnames": "2.5.1",
|
||||||
"classnames": "2.3.2",
|
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"connected-react-router": "6.9.3",
|
"connected-react-router": "6.9.3",
|
||||||
"element-class": "0.2.2",
|
"element-class": "0.2.2",
|
||||||
"filesize": "10.0.7",
|
"filesize": "10.1.6",
|
||||||
"fuse.js": "6.6.2",
|
"fuse.js": "6.6.2",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"jdu": "1.0.0",
|
"jdu": "1.0.0",
|
||||||
"jquery": "3.7.0",
|
"jquery": "3.7.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mobile-detect": "1.4.5",
|
"mobile-detect": "1.4.5",
|
||||||
"moment": "2.29.4",
|
"moment": "2.30.1",
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"qs": "6.11.1",
|
"qs": "6.13.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-addons-shallow-compare": "15.6.3",
|
"react-addons-shallow-compare": "15.6.3",
|
||||||
"react-async-script": "1.2.0",
|
"react-async-script": "1.2.0",
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
"react-dnd-touch-backend": "14.1.1",
|
"react-dnd-touch-backend": "14.1.1",
|
||||||
"react-document-title": "2.0.3",
|
"react-document-title": "2.0.3",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-focus-lock": "2.5.2",
|
"react-focus-lock": "2.9.4",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-lazyload": "3.2.0",
|
"react-lazyload": "3.2.0",
|
||||||
"react-measure": "2.5.2",
|
"react-measure": "2.5.2",
|
||||||
@@ -73,80 +72,77 @@
|
|||||||
"react-redux": "7.2.4",
|
"react-redux": "7.2.4",
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
"react-slider": "1.3.1",
|
"react-slider": "1.3.3",
|
||||||
"react-tabs": "3.2.2",
|
"react-tabs": "4.3.0",
|
||||||
"react-text-truncate": "0.18.0",
|
"react-text-truncate": "0.19.0",
|
||||||
"react-virtualized": "9.21.1",
|
"react-virtualized": "9.21.1",
|
||||||
"redux": "4.1.0",
|
"redux": "4.2.1",
|
||||||
"redux-actions": "2.6.5",
|
"redux-actions": "2.6.5",
|
||||||
"redux-batched-actions": "0.5.0",
|
"redux-batched-actions": "0.5.0",
|
||||||
"redux-localstorage": "0.4.1",
|
"redux-localstorage": "0.4.1",
|
||||||
"redux-thunk": "2.3.0",
|
"redux-thunk": "2.4.2",
|
||||||
"reselect": "4.1.8",
|
"reselect": "4.1.8",
|
||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.24.4",
|
"@babel/core": "7.26.0",
|
||||||
"@babel/eslint-parser": "7.24.1",
|
"@babel/eslint-parser": "7.25.9",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
"@babel/plugin-proposal-export-default-from": "7.25.9",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.24.4",
|
"@babel/preset-env": "7.26.0",
|
||||||
"@babel/preset-react": "7.24.1",
|
"@babel/preset-react": "7.26.3",
|
||||||
"@babel/preset-typescript": "7.24.1",
|
"@babel/preset-typescript": "7.26.0",
|
||||||
"@types/lodash": "4.14.195",
|
"@types/lodash": "4.14.195",
|
||||||
"@types/react-lazyload": "3.2.0",
|
"@types/react-lazyload": "3.2.3",
|
||||||
"@types/redux-actions": "2.6.2",
|
"@types/redux-actions": "2.6.5",
|
||||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "6.21.0",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.20",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.2.1",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.37.0",
|
"core-js": "3.39.0",
|
||||||
"css-loader": "6.8.1",
|
"css-loader": "6.8.1",
|
||||||
"css-modules-typescript-loader": "4.0.1",
|
"css-modules-typescript-loader": "4.0.1",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-prettier": "8.10.0",
|
"eslint-config-prettier": "8.10.0",
|
||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-json": "3.1.0",
|
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-react": "7.34.1",
|
"eslint-plugin-react": "7.37.1",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.2",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"filemanager-webpack-plugin": "8.0.0",
|
"filemanager-webpack-plugin": "8.0.0",
|
||||||
"fork-ts-checker-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",
|
"loader-utils": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.9.1",
|
||||||
"postcss": "8.4.38",
|
"postcss": "8.4.47",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
"postcss-loader": "7.3.0",
|
"postcss-loader": "7.3.0",
|
||||||
"postcss-mixins": "9.0.4",
|
"postcss-mixins": "9.0.4",
|
||||||
"postcss-nested": "6.0.1",
|
"postcss-nested": "6.2.0",
|
||||||
"postcss-simple-vars": "7.0.1",
|
"postcss-simple-vars": "7.0.1",
|
||||||
"postcss-url": "10.1.3",
|
"postcss-url": "10.1.3",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"require-nocache": "1.0.0",
|
"require-nocache": "1.0.0",
|
||||||
"rimraf": "4.4.1",
|
"rimraf": "6.0.1",
|
||||||
"run-sequence": "2.2.1",
|
"style-loader": "3.3.4",
|
||||||
"streamqueue": "1.1.2",
|
|
||||||
"style-loader": "3.3.3",
|
|
||||||
"stylelint": "15.10.3",
|
"stylelint": "15.10.3",
|
||||||
"stylelint-order": "6.0.3",
|
"stylelint-order": "6.0.4",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.10",
|
||||||
"ts-loader": "9.4.4",
|
"ts-loader": "9.5.1",
|
||||||
"typescript-plugin-css-modules": "5.0.1",
|
"typescript-plugin-css-modules": "5.0.1",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.95.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-livereload-plugin": "3.0.2",
|
"webpack-livereload-plugin": "3.0.2",
|
||||||
"worker-loader": "3.0.8"
|
"worker-loader": "3.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "16.17.0",
|
"node": "20.11.1",
|
||||||
"yarn": "1.22.19"
|
"yarn": "1.22.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,35 @@
|
|||||||
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
|
<RootNamespace Condition="'$(ReadarrProject)'=='true'">$(MSBuildProjectName.Replace('Readarr','NzbDrone'))</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TestProject)'!='true'">
|
||||||
|
<!-- Annotates .NET assemblies with repository information including SHA -->
|
||||||
|
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
||||||
|
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
||||||
|
<PackageReference Include="Microsoft.SourceLink.GitHub" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Sentry specific configuration: Only in Release mode -->
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
||||||
|
<!-- OrgSlug, ProjectSlug and AuthToken are required.
|
||||||
|
They can be set below, via argument to 'msbuild -p:' or environment variable -->
|
||||||
|
<SentryOrg></SentryOrg>
|
||||||
|
<SentryProject></SentryProject>
|
||||||
|
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
|
||||||
|
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
|
||||||
|
|
||||||
|
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
|
||||||
|
without the need to deploy the application with PDBs -->
|
||||||
|
<SentryUploadSymbols>true</SentryUploadSymbols>
|
||||||
|
|
||||||
|
<!-- Source Link settings -->
|
||||||
|
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
|
||||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
|
||||||
|
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
|
||||||
|
<EmbedAllSources>true</EmbedAllSources>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
|||||||
@@ -4,26 +4,30 @@
|
|||||||
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
||||||
<PackageVersion Include="Dapper" Version="2.0.151" />
|
<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.dll" Version="5.4.3" />
|
||||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageVersion Include="Polly" Version="8.4.1" />
|
<PackageVersion Include="IPAddressRange" Version="6.1.0" />
|
||||||
|
<PackageVersion Include="Polly" Version="8.5.2" />
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
<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.SQLite" Version="3.3.2.9" />
|
||||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" 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="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="ImpromptuInterface" Version="7.0.1" />
|
||||||
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
||||||
<PackageVersion Include="Mailkit" Version="3.6.0" />
|
<PackageVersion Include="Mailkit" Version="4.8.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.32" />
|
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
<PackageVersion Include="Microsoft.Data.SqlClient" Version="2.1.7" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" 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.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
|
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
||||||
<PackageVersion Include="Moq" Version="4.17.2" />
|
<PackageVersion Include="Moq" Version="4.17.2" />
|
||||||
@@ -33,36 +37,36 @@
|
|||||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
|
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
|
||||||
<PackageVersion Include="NLog" Version="5.1.4" />
|
<PackageVersion Include="NLog" Version="5.1.4" />
|
||||||
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageVersion Include="Npgsql" Version="7.0.7" />
|
<PackageVersion Include="Npgsql" Version="7.0.10" />
|
||||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageVersion Include="NUnit" Version="3.14.0" />
|
<PackageVersion Include="NUnit" Version="3.14.0" />
|
||||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
<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.Serializers.SystemTextJson" Version="106.15.0" />
|
||||||
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
||||||
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
||||||
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
|
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" />
|
||||||
<PackageVersion Include="Sentry" Version="3.31.0" />
|
<PackageVersion Include="Sentry" Version="4.0.2" />
|
||||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
||||||
<PackageVersion Include="System.Buffers" Version="4.5.1" />
|
<PackageVersion Include="System.Buffers" Version="4.6.0" />
|
||||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||||
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
||||||
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
||||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageVersion Include="System.Memory" Version="4.5.5" />
|
<PackageVersion Include="System.Memory" Version="4.6.2" />
|
||||||
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||||
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
||||||
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
|
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
<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="System.ValueTuple" Version="4.6.1" />
|
||||||
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -21,9 +21,28 @@ namespace NzbDrone.Common.Test.ExtensionTests
|
|||||||
[TestCase("1.2.3.4")]
|
[TestCase("1.2.3.4")]
|
||||||
[TestCase("172.55.0.1")]
|
[TestCase("172.55.0.1")]
|
||||||
[TestCase("192.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)
|
public void should_return_false_for_public_ip_address(string ipAddress)
|
||||||
{
|
{
|
||||||
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
|
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")]
|
||||||
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
[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)
|
public void should_clean_message(string message)
|
||||||
{
|
{
|
||||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Instrumentation.Sentry;
|
using NzbDrone.Common.Instrumentation.Sentry;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
|
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
||||||
|
|||||||
@@ -42,17 +42,18 @@ namespace NzbDrone.Common
|
|||||||
|
|
||||||
public void CreateZip(string path, IEnumerable<string> files)
|
public void CreateZip(string path, IEnumerable<string> files)
|
||||||
{
|
{
|
||||||
using (var zipFile = ZipFile.Create(path))
|
_logger.Debug("Creating archive {0}", path);
|
||||||
|
|
||||||
|
using var zipFile = ZipFile.Create(path);
|
||||||
|
|
||||||
|
zipFile.BeginUpdate();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
zipFile.BeginUpdate();
|
zipFile.Add(file, Path.GetFileName(file));
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
zipFile.Add(file, Path.GetFileName(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
zipFile.CommitUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zipFile.CommitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExtractZip(string compressedFile, string destination)
|
private void ExtractZip(string compressedFile, string destination)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
@@ -306,9 +307,26 @@ namespace NzbDrone.Common.Disk
|
|||||||
{
|
{
|
||||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||||
|
|
||||||
var files = GetFiles(path, recursive);
|
var files = GetFiles(path, recursive).ToList();
|
||||||
|
|
||||||
files.ToList().ForEach(RemoveReadOnly);
|
files.ForEach(RemoveReadOnly);
|
||||||
|
|
||||||
|
var attempts = 0;
|
||||||
|
|
||||||
|
while (attempts < 3 && files.Any())
|
||||||
|
{
|
||||||
|
EmptyFolder(path);
|
||||||
|
|
||||||
|
if (GetFiles(path, recursive).Any())
|
||||||
|
{
|
||||||
|
// Wait for IO operations to complete after emptying the folder since they aren't always
|
||||||
|
// instantly removed and it can lead to false positives that files are still present.
|
||||||
|
Thread.Sleep(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
files = GetFiles(path, recursive).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
_fileSystem.Directory.Delete(path, recursive);
|
_fileSystem.Directory.Delete(path, recursive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,10 +342,11 @@ namespace NzbDrone.Common.Disk
|
|||||||
|
|
||||||
var isCifs = targetDriveFormat == "cifs";
|
var isCifs = targetDriveFormat == "cifs";
|
||||||
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
||||||
|
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
|
||||||
|
|
||||||
if (mode.HasFlag(TransferMode.Copy))
|
if (mode.HasFlag(TransferMode.Copy))
|
||||||
{
|
{
|
||||||
if (isBtrfs)
|
if (isBtrfs || isZfs)
|
||||||
{
|
{
|
||||||
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
||||||
{
|
{
|
||||||
@@ -359,7 +360,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
|
|
||||||
if (mode.HasFlag(TransferMode.Move))
|
if (mode.HasFlag(TransferMode.Move))
|
||||||
{
|
{
|
||||||
if (isBtrfs)
|
if (isBtrfs || isZfs)
|
||||||
{
|
{
|
||||||
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,18 +39,24 @@ namespace NzbDrone.Common.Extensions
|
|||||||
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
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)
|
// 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)
|
// 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)
|
// 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)
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,15 @@ namespace NzbDrone.Common.Extensions
|
|||||||
return Directory.GetParent(cleanPath)?.FullName;
|
return Directory.GetParent(cleanPath)?.FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetCleanPath(this string path)
|
||||||
|
{
|
||||||
|
var cleanPath = OsInfo.IsWindows
|
||||||
|
? PARENT_PATH_END_SLASH_REGEX.Replace(path, "")
|
||||||
|
: path.TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
return cleanPath;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsParentPath(this string parentPath, string childPath)
|
public static bool IsParentPath(this string parentPath, string childPath)
|
||||||
{
|
{
|
||||||
if (parentPath != "/" && !parentPath.EndsWith(":\\"))
|
if (parentPath != "/" && !parentPath.EndsWith(":\\"))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http.Proxy
|
namespace NzbDrone.Common.Http.Proxy
|
||||||
@@ -29,7 +30,8 @@ namespace NzbDrone.Common.Http.Proxy
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(BypassFilter))
|
if (!string.IsNullOrWhiteSpace(BypassFilter))
|
||||||
{
|
{
|
||||||
var hostlist = BypassFilter.Split(',');
|
var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
for (var i = 0; i < hostlist.Length; i++)
|
for (var i = 0; i < hostlist.Length; i++)
|
||||||
{
|
{
|
||||||
if (hostlist[i].StartsWith("*"))
|
if (hostlist[i].StartsWith("*"))
|
||||||
@@ -41,7 +43,7 @@ namespace NzbDrone.Common.Http.Proxy
|
|||||||
return hostlist;
|
return hostlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new string[] { };
|
return Array.Empty<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
new (@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new (@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
// Discord
|
// 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);
|
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);
|
||||||
|
|||||||
@@ -4,27 +4,27 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
|||||||
{
|
{
|
||||||
public static class LoggerExtensions
|
public static class LoggerExtensions
|
||||||
{
|
{
|
||||||
|
[MessageTemplateFormatMethod("message")]
|
||||||
public static void ProgressInfo(this Logger logger, string message, params object[] args)
|
public static void ProgressInfo(this Logger logger, string message, params object[] args)
|
||||||
{
|
{
|
||||||
var formattedMessage = string.Format(message, args);
|
LogProgressMessage(logger, LogLevel.Info, message, args);
|
||||||
LogProgressMessage(logger, LogLevel.Info, formattedMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MessageTemplateFormatMethod("message")]
|
||||||
public static void ProgressDebug(this Logger logger, string message, params object[] args)
|
public static void ProgressDebug(this Logger logger, string message, params object[] args)
|
||||||
{
|
{
|
||||||
var formattedMessage = string.Format(message, args);
|
LogProgressMessage(logger, LogLevel.Debug, message, args);
|
||||||
LogProgressMessage(logger, LogLevel.Debug, formattedMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MessageTemplateFormatMethod("message")]
|
||||||
public static void ProgressTrace(this Logger logger, string message, params object[] args)
|
public static void ProgressTrace(this Logger logger, string message, params object[] args)
|
||||||
{
|
{
|
||||||
var formattedMessage = string.Format(message, args);
|
LogProgressMessage(logger, LogLevel.Trace, message, args);
|
||||||
LogProgressMessage(logger, LogLevel.Trace, formattedMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogProgressMessage(Logger logger, LogLevel level, string message)
|
private static void LogProgressMessage(Logger logger, LogLevel level, string message, object[] parameters)
|
||||||
{
|
{
|
||||||
var logEvent = new LogEventInfo(level, logger.Name, message);
|
var logEvent = new LogEventInfo(level, logger.Name, null, message, parameters);
|
||||||
logEvent.Properties.Add("Status", "");
|
logEvent.Properties.Add("Status", "");
|
||||||
|
|
||||||
logger.Log(logEvent);
|
logger.Log(logEvent);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
RegisterDebugger();
|
RegisterDebugger();
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterSentry(updateApp);
|
RegisterSentry(updateApp, appFolderInfo);
|
||||||
|
|
||||||
if (updateApp)
|
if (updateApp)
|
||||||
{
|
{
|
||||||
@@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
LogManager.ReconfigExistingLoggers();
|
LogManager.ReconfigExistingLoggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterSentry(bool updateClient)
|
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
string dsn;
|
string dsn;
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
|
: "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4";
|
||||||
}
|
}
|
||||||
|
|
||||||
var target = new SentryTarget(dsn)
|
var target = new SentryTarget(dsn, appFolderInfo)
|
||||||
{
|
{
|
||||||
Name = "sentryTarget",
|
Name = "sentryTarget",
|
||||||
Layout = "${message}"
|
Layout = "${message}"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using NLog;
|
|||||||
using NLog.Common;
|
using NLog.Common;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
@@ -99,7 +100,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
public bool FilterEvents { get; set; }
|
public bool FilterEvents { get; set; }
|
||||||
public bool SentryEnabled { get; set; }
|
public bool SentryEnabled { get; set; }
|
||||||
|
|
||||||
public SentryTarget(string dsn)
|
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
_sdk = SentrySdk.Init(o =>
|
_sdk = SentrySdk.Init(o =>
|
||||||
{
|
{
|
||||||
@@ -107,9 +108,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.AttachStacktrace = true;
|
o.AttachStacktrace = true;
|
||||||
o.MaxBreadcrumbs = 200;
|
o.MaxBreadcrumbs = 200;
|
||||||
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
||||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
|
||||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
|
||||||
o.Environment = BuildInfo.Branch;
|
o.Environment = BuildInfo.Branch;
|
||||||
|
|
||||||
|
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
||||||
|
o.AutoSessionTracking = false;
|
||||||
|
|
||||||
|
// Caches files in the event device is offline
|
||||||
|
// Sentry creates a 'sentry' sub directory, no need to concat here
|
||||||
|
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
|
||||||
|
|
||||||
|
// default environment is production
|
||||||
|
if (!RuntimeInfo.IsProduction)
|
||||||
|
{
|
||||||
|
if (RuntimeInfo.IsDevelopment)
|
||||||
|
{
|
||||||
|
o.Environment = "development";
|
||||||
|
}
|
||||||
|
else if (RuntimeInfo.IsTesting)
|
||||||
|
{
|
||||||
|
o.Environment = "testing";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
o.Environment = "other";
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
InitializeScope();
|
InitializeScope();
|
||||||
@@ -127,7 +152,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
{
|
{
|
||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
scope.User = new User
|
scope.User = new SentryUser
|
||||||
{
|
{
|
||||||
Id = HashUtil.AnonymousToken()
|
Id = HashUtil.AnonymousToken()
|
||||||
};
|
};
|
||||||
@@ -169,9 +194,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
|
|
||||||
private void OnError(Exception ex)
|
private void OnError(Exception ex)
|
||||||
{
|
{
|
||||||
var webException = ex as WebException;
|
if (ex is WebException webException)
|
||||||
|
|
||||||
if (webException != null)
|
|
||||||
{
|
{
|
||||||
var response = webException.Response as HttpWebResponse;
|
var response = webException.Response as HttpWebResponse;
|
||||||
var statusCode = response?.StatusCode;
|
var statusCode = response?.StatusCode;
|
||||||
@@ -290,13 +313,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var level = LoggingLevelMap[logEvent.Level];
|
||||||
var sentryEvent = new SentryEvent(logEvent.Exception)
|
var sentryEvent = new SentryEvent(logEvent.Exception)
|
||||||
{
|
{
|
||||||
Level = LoggingLevelMap[logEvent.Level],
|
Level = level,
|
||||||
Logger = logEvent.LoggerName,
|
Logger = logEvent.LoggerName,
|
||||||
Message = logEvent.FormattedMessage
|
Message = logEvent.FormattedMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
|
||||||
|
{
|
||||||
|
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
|
||||||
|
// the 'unhandled' exception flag
|
||||||
|
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
|
||||||
|
}
|
||||||
|
|
||||||
sentryEvent.SetExtras(extras);
|
sentryEvent.SetExtras(extras);
|
||||||
sentryEvent.SetFingerprint(fingerPrint);
|
sentryEvent.SetFingerprint(fingerPrint);
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ public class AuthOptions
|
|||||||
public bool? Enabled { get; set; }
|
public bool? Enabled { get; set; }
|
||||||
public string Method { get; set; }
|
public string Method { get; set; }
|
||||||
public string Required { get; set; }
|
public string Required { get; set; }
|
||||||
|
public bool? TrustCgnatIpAddresses { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.ComponentModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Model;
|
using NzbDrone.Common.Model;
|
||||||
@@ -117,7 +118,9 @@ namespace NzbDrone.Common.Processes
|
|||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardInput = true
|
RedirectStandardInput = true,
|
||||||
|
StandardOutputEncoding = Encoding.UTF8,
|
||||||
|
StandardErrorEncoding = Encoding.UTF8
|
||||||
};
|
};
|
||||||
|
|
||||||
if (environmentVariables != null)
|
if (environmentVariables != null)
|
||||||
@@ -313,7 +316,7 @@ namespace NzbDrone.Common.Processes
|
|||||||
processInfo = new ProcessInfo();
|
processInfo = new ProcessInfo();
|
||||||
processInfo.Id = process.Id;
|
processInfo.Id = process.Id;
|
||||||
processInfo.Name = process.ProcessName;
|
processInfo.Name = process.ProcessName;
|
||||||
processInfo.StartPath = process.MainModule.FileName;
|
processInfo.StartPath = process.MainModule?.FileName;
|
||||||
|
|
||||||
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" />
|
<PackageReference Include="DryIoc.dll" />
|
||||||
|
<PackageReference Include="IPAddressRange" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" />
|
<PackageReference Include="NLog.Extensions.Logging" />
|
||||||
|
|||||||
@@ -200,17 +200,9 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
var seriesTags = new HashSet<int> { 2 };
|
var seriesTags = new HashSet<int> { 2 };
|
||||||
var clientTags = new HashSet<int> { 1 };
|
var clientTags = new HashSet<int> { 1 };
|
||||||
|
|
||||||
WithTorrentClient(0, clientTags);
|
|
||||||
WithTorrentClient(0, clientTags);
|
|
||||||
WithTorrentClient(0, clientTags);
|
|
||||||
WithTorrentClient(0, clientTags);
|
WithTorrentClient(0, clientTags);
|
||||||
|
|
||||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
Assert.Throws<DownloadClientUnavailableException>(() => 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -312,11 +312,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_status_with_outputdirs()
|
public void should_return_status_with_outputdirs()
|
||||||
{
|
{
|
||||||
var configItems = new Dictionary<string, object>();
|
var configItems = new Dictionary<string, object>
|
||||||
|
{
|
||||||
configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic());
|
{ "download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic() },
|
||||||
configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
{ "move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic() },
|
||||||
configItems.Add("move_completed", true);
|
{ "move_completed", true }
|
||||||
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IDelugeProxy>()
|
Mocker.GetMock<IDelugeProxy>()
|
||||||
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
|
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
|
||||||
@@ -328,5 +329,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
|||||||
result.OutputRootFolders.Should().NotBeNull();
|
result.OutputRootFolders.Should().NotBeNull();
|
||||||
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-51
@@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
VerifyWarning(item);
|
VerifyWarning(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedDL")]
|
||||||
public void paused_item_should_have_required_properties()
|
[TestCase("stoppedDL")]
|
||||||
|
public void paused_item_should_have_required_properties(string state)
|
||||||
{
|
{
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
@@ -188,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Size = 1000,
|
Size = 1000,
|
||||||
Progress = 0.7,
|
Progress = 0.7,
|
||||||
Eta = 8640000,
|
Eta = 8640000,
|
||||||
State = "pausedDL",
|
State = state,
|
||||||
Label = "",
|
Label = "",
|
||||||
SavePath = ""
|
SavePath = ""
|
||||||
};
|
};
|
||||||
@@ -200,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("pausedUP")]
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
[TestCase("queuedUP")]
|
[TestCase("queuedUP")]
|
||||||
[TestCase("uploading")]
|
[TestCase("uploading")]
|
||||||
[TestCase("stalledUP")]
|
[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"));
|
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void api_261_should_use_content_path()
|
[TestCase("stoppedUP")]
|
||||||
|
public void api_261_should_use_content_path(string state)
|
||||||
{
|
{
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
@@ -407,7 +410,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Size = 1000,
|
Size = 1000,
|
||||||
Progress = 0.7,
|
Progress = 0.7,
|
||||||
Eta = 8640000,
|
Eta = 8640000,
|
||||||
State = "pausedUP",
|
State = state,
|
||||||
Label = "",
|
Label = "",
|
||||||
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
||||||
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
||||||
@@ -684,44 +687,96 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeFalse();
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(-1);
|
GivenGlobalSeedLimits(-1);
|
||||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
item.CanMoveFiles.Should().BeFalse();
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 1.1006066990976857f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_just_under_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 0.9999f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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);
|
GivenGlobalSeedLimits(2.0f);
|
||||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
|
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(2.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 1.1006066990976857f, ratioLimit: 1.1f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_just_under_overridden_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(2.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 0.9999f, ratioLimit: 1.0f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(0.2f);
|
GivenGlobalSeedLimits(0.2f);
|
||||||
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
|
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
@@ -739,33 +794,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeFalse();
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(-1, 20);
|
GivenGlobalSeedLimits(-1, 20);
|
||||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(-1, 40);
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(-1, 20);
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
@@ -783,66 +841,72 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeFalse();
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
|
[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);
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
|
[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);
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
item.CanMoveFiles.Should().BeFalse();
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
|
[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);
|
GivenGlobalSeedLimits(2.0f, 20);
|
||||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
|
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
|
[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);
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_not_fetch_details_twice()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_not_fetch_details_twice(string state)
|
||||||
{
|
{
|
||||||
GivenGlobalSeedLimits(-1, 30);
|
GivenGlobalSeedLimits(-1, 30);
|
||||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
@@ -854,8 +918,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
|
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_get_category_from_the_category_if_set()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_get_category_from_the_category_if_set(string state)
|
||||||
{
|
{
|
||||||
const string category = "music-readarr";
|
const string category = "music-readarr";
|
||||||
GivenGlobalSeedLimits(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
@@ -867,7 +932,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Size = 1000,
|
Size = 1000,
|
||||||
Progress = 1.0,
|
Progress = 1.0,
|
||||||
Eta = 8640000,
|
Eta = 8640000,
|
||||||
State = "pausedUP",
|
State = state,
|
||||||
Category = category,
|
Category = category,
|
||||||
SavePath = "",
|
SavePath = "",
|
||||||
Ratio = 1.0f
|
Ratio = 1.0f
|
||||||
@@ -879,8 +944,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.Category.Should().Be(category);
|
item.Category.Should().Be(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase("pausedUP")]
|
||||||
public void should_get_category_from_the_label_if_the_category_is_not_available()
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
|
||||||
{
|
{
|
||||||
const string category = "music-readarr";
|
const string category = "music-readarr";
|
||||||
GivenGlobalSeedLimits(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
@@ -892,7 +958,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Size = 1000,
|
Size = 1000,
|
||||||
Progress = 1.0,
|
Progress = 1.0,
|
||||||
Eta = 8640000,
|
Eta = 8640000,
|
||||||
State = "pausedUP",
|
State = state,
|
||||||
Label = category,
|
Label = category,
|
||||||
SavePath = "",
|
SavePath = "",
|
||||||
Ratio = 1.0f
|
Ratio = 1.0f
|
||||||
|
|||||||
@@ -478,6 +478,37 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
|||||||
downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue();
|
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\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(@"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")]
|
[TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")]
|
||||||
|
|||||||
+5
-2
@@ -49,10 +49,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void magnet_download_should_not_return_the_item()
|
public void magnet_download_should_be_returned_as_queued()
|
||||||
{
|
{
|
||||||
PrepareClientToReturnMagnetItem();
|
PrepareClientToReturnMagnetItem();
|
||||||
Subject.GetItems().Count().Should().Be(0);
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
|
|||||||
public void magnet_download_should_not_return_the_item()
|
public void magnet_download_should_not_return_the_item()
|
||||||
{
|
{
|
||||||
PrepareClientToReturnMagnetItem();
|
PrepareClientToReturnMagnetItem();
|
||||||
Subject.GetItems().Count().Should().Be(0);
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(DownloadItemStatus.Queued);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using NzbDrone.Core.HealthCheck.Checks;
|
|||||||
using NzbDrone.Core.Localization;
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Core.Update;
|
using NzbDrone.Core.Update;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||||
{
|
{
|
||||||
@@ -21,28 +22,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
.Returns("Some Warning Message");
|
.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]
|
[Test]
|
||||||
public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled()
|
public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||||
{
|
{
|
||||||
PosixOnly();
|
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||||
|
|
||||||
const string startupFolder = @"/opt/nzbdrone";
|
|
||||||
|
|
||||||
Mocker.GetMock<IConfigFileProvider>()
|
Mocker.GetMock<IConfigFileProvider>()
|
||||||
.Setup(s => s.UpdateAutomatically)
|
.Setup(s => s.UpdateAutomatically)
|
||||||
@@ -62,10 +45,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled()
|
public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled()
|
||||||
{
|
{
|
||||||
PosixOnly();
|
var startupFolder = @"C:\NzbDrone".AsOsAgnostic();
|
||||||
|
var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic();
|
||||||
const string startupFolder = @"/opt/nzbdrone";
|
|
||||||
const string uiFolder = @"/opt/nzbdrone/UI";
|
|
||||||
|
|
||||||
Mocker.GetMock<IConfigFileProvider>()
|
Mocker.GetMock<IConfigFileProvider>()
|
||||||
.Setup(s => s.UpdateAutomatically)
|
.Setup(s => s.UpdateAutomatically)
|
||||||
@@ -89,7 +70,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled()
|
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>()
|
Mocker.GetMock<IConfigFileProvider>()
|
||||||
.Setup(s => s.UpdateAutomatically)
|
.Setup(s => s.UpdateAutomatically)
|
||||||
@@ -101,7 +82,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
|
|
||||||
Mocker.GetMock<IAppFolderInfo>()
|
Mocker.GetMock<IAppFolderInfo>()
|
||||||
.Setup(s => s.StartUpFolder)
|
.Setup(s => s.StartUpFolder)
|
||||||
.Returns(@"/opt/nzbdrone");
|
.Returns(startupFolder);
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Verify(c => c.FolderWritable(It.IsAny<string>()), Times.Never());
|
.Verify(c => c.FolderWritable(It.IsAny<string>()), Times.Never());
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user