mirror of https://github.com/lidarr/Lidarr
Compare commits
68 Commits
v2.2.3.409
...
develop
Author | SHA1 | Date |
---|---|---|
Bogdan | d27b062d6a | |
Bogdan | febb3ef485 | |
sillock1 | 30d9891bf0 | |
Bogdan | 2621acdae5 | |
Bogdan | 18772553f2 | |
Weblate | 0300bf2dd2 | |
Bogdan | 77861e4303 | |
Bogdan | 3545a7451e | |
Mark McDowall | 9f8c4530ca | |
Mickaël Thomas | 9da690f807 | |
Bogdan | 31f342b8ad | |
Bogdan | 596a36d45f | |
Bogdan | 94bb8a436b | |
Weblate | 94d2a20b6a | |
Bogdan | a25e5aae10 | |
Mark McDowall | f4a02ffc83 | |
Bogdan | 1bdcf91014 | |
Bogdan | 4d28d3f25a | |
Bogdan | 9660ec37cd | |
Bogdan | 66c7521f4b | |
Stevie Robinson | 8b57b33c99 | |
Mark McDowall | 580e4becbe | |
Christopher | 5f248aa25e | |
Bogdan | a735eccb65 | |
Weblate | d11ed42830 | |
Bogdan | b0038dd143 | |
Bogdan | 2e242aeb7b | |
Bogdan | 416d505316 | |
Bogdan | 4816f35256 | |
Josh McKinney | e42e0a72eb | |
Bogdan | db9e62f79d | |
Bogdan | bc69fa4842 | |
Bogdan | 86dad72c49 | |
Bogdan | 4a8d6c367d | |
Bogdan | c1926f8758 | |
Bogdan | 7820bcf91f | |
Servarr | 431ad0a028 | |
Bogdan | 59cf7a95c3 | |
Bogdan | e17e3633f8 | |
Bogdan | 46da2b49c6 | |
Josh McKinney | 3071977284 | |
Mark McDowall | b14e2bb618 | |
Mark McDowall | 8c09c0cb5c | |
Weblate | 8cebb21c2d | |
Bogdan | 74ac263b74 | |
Bogdan | adcec90ef8 | |
Qstick | daf8b94c8e | |
Bogdan | 7c4f0c597e | |
Bogdan | 1d2af2aab4 | |
Stevie Robinson | 5d537689fb | |
Cuki | ca6beea62b | |
Mark McDowall | a82c919093 | |
Mark McDowall | 2941e0c4b7 | |
Bogdan | ca0b900d92 | |
Weblate | 72f1b2075b | |
Bogdan | e847828191 | |
Bogdan | 2a10505dff | |
Mark McDowall | 28f2eb974d | |
Servarr | 13ce040e4d | |
Weblate | f477f9b287 | |
Bogdan | 0e84008669 | |
Louis R | 52b5ff6fdd | |
Carlos Gustavo Sarmiento | 1d0de51917 | |
Stevie Robinson | a8648fdb71 | |
Bogdan | f890a8c18f | |
Mark McDowall | e730cf6307 | |
Bogdan | 9f4d821a2d | |
Weblate | ce6e4555ec |
|
@ -0,0 +1,13 @@
|
|||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Lidarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8686],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
|
@ -126,6 +126,7 @@ coverage*.xml
|
|||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Lidarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Lidarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Lidarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,13 +9,13 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.2.3'
|
||||
majorVersion: '2.4.0'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
dotnetVersion: '6.0.421'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
|
@ -166,10 +166,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
@ -1093,10 +1093,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
|
|
@ -28,7 +28,8 @@ module.exports = {
|
|||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false
|
||||
sinon: false,
|
||||
JSX: true
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
|
|
@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
title,
|
||||
statistics,
|
||||
statistics = {},
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
|
|||
title,
|
||||
artistName,
|
||||
albumType,
|
||||
statistics,
|
||||
statistics = {},
|
||||
item,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
|
|
|
@ -7,6 +7,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AlbumInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
|
@ -18,7 +19,10 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||
{albumTitle === undefined ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
|
@ -32,7 +36,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
@ -12,11 +12,10 @@ function App({ store, history }) {
|
|||
<DocumentTitle title={window.Lidarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
@ -0,0 +1,37 @@
|
|||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
interface ApplyThemeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
|
@ -5,6 +5,7 @@ import CommandAppState from './CommandAppState';
|
|||
import HistoryAppState from './HistoryAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
import TrackFilesAppState from './TrackFilesAppState';
|
||||
import TracksAppState from './TracksAppState';
|
||||
|
@ -62,6 +63,7 @@ interface AppState {
|
|||
tags: TagsAppState;
|
||||
trackFiles: TrackFilesAppState;
|
||||
tracksSelection: TracksAppState;
|
||||
system: SystemAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
@ -46,7 +47,7 @@ export interface RootFolderAppState
|
|||
AppSectionSaveState {}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
|
@ -57,7 +58,7 @@ interface SettingsAppState {
|
|||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
|
||||
interface SystemAppState {
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
|
@ -1,12 +1,32 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
export interface TagDetail extends ModelBase {
|
||||
label: string;
|
||||
autoTagIds: number[];
|
||||
delayProfileIds: number[];
|
||||
downloadClientIds: [];
|
||||
importListIds: number[];
|
||||
indexerIds: number[];
|
||||
notificationIds: number[];
|
||||
restrictionIds: number[];
|
||||
artistIds: number[];
|
||||
}
|
||||
|
||||
export interface TagDetailAppState
|
||||
extends AppSectionState<TagDetail>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
|
||||
details: TagDetailAppState;
|
||||
}
|
||||
|
||||
export default TagsAppState;
|
||||
|
|
|
@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteArtistConfirmed}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
@ -161,9 +161,7 @@ DeleteArtistModalContent.propTypes = {
|
|||
};
|
||||
|
||||
DeleteArtistModalContent.defaultProps = {
|
||||
statistics: {
|
||||
trackFileCount: 0
|
||||
}
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default DeleteArtistModalContent;
|
||||
|
|
|
@ -192,7 +192,7 @@ class ArtistDetails extends Component {
|
|||
artistName,
|
||||
ratings,
|
||||
path,
|
||||
statistics,
|
||||
statistics = {},
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
genres,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Statistics } from 'Album/Album';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
@ -56,7 +57,7 @@ function AlbumDetails(props: AlbumDetailsProps) {
|
|||
disambiguation,
|
||||
albumType,
|
||||
monitored,
|
||||
statistics,
|
||||
statistics = {} as Statistics,
|
||||
isSaving = false,
|
||||
} = album;
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
|
||||
interface ArtistTagInputProps {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
onChange: ({
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function ArtistTagInput(props: ArtistTagInputProps) {
|
||||
const { value, onChange, ...otherProps } = props;
|
||||
const isArray = Array.isArray(value);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||
if (isArray) {
|
||||
onChange({ name, value: newValue });
|
||||
} else {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isArray, onChange]
|
||||
);
|
||||
|
||||
let finalValue: number[] = [];
|
||||
|
||||
if (isArray) {
|
||||
finalValue = value;
|
||||
} else if (value === 0) {
|
||||
finalValue = [];
|
||||
} else {
|
||||
finalValue = [value];
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||
<TagInputConnector
|
||||
{...otherProps}
|
||||
value={finalValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,7 @@ import Link from 'Components/Link/Link';
|
|||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
|
||||
import ArtistTagInput from './ArtistTagInput';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
|
@ -99,6 +100,9 @@ function getComponent(type) {
|
|||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.ARTIST_TAG:
|
||||
return ArtistTagInput;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
font-family: $passwordFamily;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'input': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -1,7 +1,5 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
|
@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type="password"
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
className: styles.input
|
||||
...TextInput.props
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
|
|
@ -29,6 +29,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'artistTag':
|
||||
return inputTypes.ARTIST_TAG;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
case 'tagSelect':
|
||||
|
|
|
@ -25,14 +25,3 @@
|
|||
font-family: 'Ubuntu Mono';
|
||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* text-security-disc
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: 'text-security-disc';
|
||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -15,5 +15,5 @@
|
|||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "minimal-ui"
|
||||
"display": "standalone"
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
|||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const SERIES_TYPE_SELECT = 'artistTypeSelect';
|
||||
export const ARTIST_TAG = 'artistTag';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
export const TAG = 'tag';
|
||||
export const TAG_SELECT = 'tagSelect';
|
||||
|
@ -49,6 +50,7 @@ export const all = [
|
|||
DOWNLOAD_CLIENT_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
ARTIST_TAG,
|
||||
DYNAMIC_SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
TAG,
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import shortenList from 'Utilities/String/shortenList';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
|
@ -56,8 +55,7 @@ class SelectAlbumReleaseRow extends Component {
|
|||
if (name === 'release') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
<SelectInput
|
||||
name={id.toString()}
|
||||
values={_.map(releases, (r) => ({
|
||||
key: r.id,
|
||||
|
|
|
@ -48,14 +48,16 @@ class InteractiveImportRow extends Component {
|
|||
artist,
|
||||
album,
|
||||
tracks,
|
||||
quality
|
||||
quality,
|
||||
size
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
artist &&
|
||||
album != null &&
|
||||
tracks.length &&
|
||||
quality
|
||||
quality &&
|
||||
size > 0
|
||||
) {
|
||||
this.props.onSelectedChange({ id, value: true });
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
|
@ -150,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
|
|||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('CustomFormatsSettingsTriggerInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
specifications.map((tag) => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
|
@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteDownloadClientPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
@ -206,6 +208,12 @@ class EditDownloadClientModalContent extends Component {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -246,6 +254,7 @@ EditDownloadClientModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
|
||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||
setDownloadClientValue,
|
||||
setDownloadClientFieldValue,
|
||||
saveDownloadClient,
|
||||
testDownloadClient
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
|
@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
|||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
testDownloadClient: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditImportListModalContent.css';
|
||||
|
@ -66,6 +67,7 @@ function EditImportListModalContent(props) {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteImportListPress,
|
||||
showMetadataProfile,
|
||||
...otherProps
|
||||
|
@ -333,6 +335,12 @@ function EditImportListModalContent(props) {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -373,6 +381,7 @@ EditImportListModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteImportListPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveImportList,
|
||||
setImportListFieldValue,
|
||||
setImportListValue,
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditImportListModalContent from './EditImportListModalContent';
|
||||
|
||||
|
@ -25,7 +31,8 @@ const mapDispatchToProps = {
|
|||
setImportListValue,
|
||||
setImportListFieldValue,
|
||||
saveImportList,
|
||||
testImportList
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditImportListModalContentConnector extends Component {
|
||||
|
@ -58,6 +65,10 @@ class EditImportListModalContentConnector extends Component {
|
|||
this.props.testImportList({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -67,6 +78,7 @@ class EditImportListModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -84,6 +96,7 @@ EditImportListModalContentConnector.propTypes = {
|
|||
setImportListFieldValue: PropTypes.func.isRequired,
|
||||
saveImportList: PropTypes.func.isRequired,
|
||||
testImportList: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ function EditIndexerModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('PriorityHelpText')}
|
||||
helpText={translate('IndexerPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
|
|
|
@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NotificationEventItems from './NotificationEventItems';
|
||||
import styles from './EditNotificationModalContent.css';
|
||||
|
@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteNotificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
@ -140,6 +142,12 @@ function EditNotificationModalContent(props) {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -179,6 +187,7 @@ EditNotificationModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteNotificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveNotification,
|
||||
setNotificationFieldValue,
|
||||
setNotificationValue,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||
|
||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||
setNotificationValue,
|
||||
setNotificationFieldValue,
|
||||
saveNotification,
|
||||
testNotification
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditNotificationModalContentConnector extends Component {
|
||||
|
@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
|||
this.props.testNotification({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
|||
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||
saveNotification: PropTypes.func.isRequired,
|
||||
testNotification: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ export default function TagInUse(props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (count > 1 && labelPlural ) {
|
||||
if (count > 1 && labelPlural) {
|
||||
return (
|
||||
<div>
|
||||
{count} {labelPlural.toLowerCase()}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastTestData = null;
|
||||
|
||||
export function createCancelTestProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
|
@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
|||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isTesting: true }));
|
||||
|
||||
const testData = getProviderState(payload, getState, section);
|
||||
const {
|
||||
queryParams = {},
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const testData = getProviderState({ ...otherPayload }, getState, section);
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-testing the same provider without changes
|
||||
// force it to be tested.
|
||||
|
||||
if (_.isEqual(testData, lastTestData)) {
|
||||
params.forceTest = true;
|
||||
}
|
||||
|
||||
lastTestData = testData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/test`,
|
||||
url: `${url}/test?${$.param(params, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
|
@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
|||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastTestData = null;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isTesting: false,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createAllArtistSelector() {
|
||||
return createSelector(
|
||||
(state) => state.artist,
|
||||
(state: AppState) => state.artist,
|
||||
(artist) => {
|
||||
return artist.items;
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createArtistCountSelector() {
|
||||
return createSelector(
|
||||
createAllArtistSelector(),
|
||||
(state) => state.artist.error,
|
||||
(state) => state.artist.isFetching,
|
||||
(state) => state.artist.isPopulated,
|
||||
(state: AppState) => state.artist.error,
|
||||
(state: AppState) => state.artist.isFetching,
|
||||
(state: AppState) => state.artist.isPopulated,
|
||||
(artists, error, isFetching, isPopulated) => {
|
||||
return {
|
||||
count: artists.length,
|
||||
error,
|
||||
isFetching,
|
||||
isPopulated
|
||||
isPopulated,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -2,13 +2,10 @@ import { createSelector } from 'reselect';
|
|||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import createCommandSelector from './createCommandSelector';
|
||||
|
||||
function createCommandExecutingSelector(name, contraints = {}) {
|
||||
return createSelector(
|
||||
createCommandSelector(name, contraints),
|
||||
(command) => {
|
||||
return isCommandExecuting(command);
|
||||
}
|
||||
);
|
||||
function createCommandExecutingSelector(name: string, contraints = {}) {
|
||||
return createSelector(createCommandSelector(name, contraints), (command) => {
|
||||
return isCommandExecuting(command);
|
||||
});
|
||||
}
|
||||
|
||||
export default createCommandExecutingSelector;
|
|
@ -1,14 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import createCommandsSelector from './createCommandsSelector';
|
||||
|
||||
function createCommandSelector(name, contraints = {}) {
|
||||
return createSelector(
|
||||
createCommandsSelector(),
|
||||
(commands) => {
|
||||
return findCommand(commands, { name, ...contraints });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createCommandSelector;
|
|
@ -0,0 +1,11 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import createCommandsSelector from './createCommandsSelector';
|
||||
|
||||
function createCommandSelector(name: string, contraints = {}) {
|
||||
return createSelector(createCommandsSelector(), (commands) => {
|
||||
return findCommand(commands, { name, ...contraints });
|
||||
});
|
||||
}
|
||||
|
||||
export default createCommandSelector;
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createCommandsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.commands,
|
||||
(state: AppState) => state.commands,
|
||||
(commands) => {
|
||||
return commands.items;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
|
||||
const createDeepEqualSelector = createSelectorCreator(
|
||||
defaultMemoize,
|
||||
_.isEqual
|
||||
);
|
||||
|
||||
export default createDeepEqualSelector;
|
|
@ -0,0 +1,6 @@
|
|||
import { isEqual } from 'lodash';
|
||||
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
|
||||
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
|
||||
|
||||
export default createDeepEqualSelector;
|
|
@ -1,9 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
|
||||
function createExecutingCommandsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.commands.items,
|
||||
(state: AppState) => state.commands.items,
|
||||
(commands) => {
|
||||
return commands.filter((command) => isCommandExecuting(command));
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
import _ from 'lodash';
|
||||
import { some } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createExistingArtistSelector() {
|
||||
return createSelector(
|
||||
(state, { foreignArtistId }) => foreignArtistId,
|
||||
(_: AppState, { foreignArtistId }: { foreignArtistId: string }) =>
|
||||
foreignArtistId,
|
||||
createAllArtistSelector(),
|
||||
(foreignArtistId, artist) => {
|
||||
return _.some(artist, { foreignArtistId });
|
||||
return some(artist, { foreignArtistId });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
function createMetadataProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { metadataProfileId }) => metadataProfileId,
|
||||
(state) => state.settings.metadataProfiles.items,
|
||||
(metadataProfileId, metadataProfiles) => {
|
||||
return metadataProfiles.find((profile) => {
|
||||
return profile.id === metadataProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMetadataProfileSelector;
|
|
@ -0,0 +1,17 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createMetadataProfileSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { metadataProfileId }: { metadataProfileId: number }) =>
|
||||
metadataProfileId,
|
||||
(state: AppState) => state.settings.metadataProfiles.items,
|
||||
(metadataProfileId, metadataProfiles) => {
|
||||
return metadataProfiles.find(
|
||||
(profile) => profile.id === metadataProfileId
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMetadataProfileSelector;
|
|
@ -1,12 +1,21 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
|
||||
function createMultiArtistsSelector(artistIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.artist.itemMap,
|
||||
(state: AppState) => state.artist.items,
|
||||
(itemMap, allArtists) => {
|
||||
return artistIds.map((artistId) => allArtists[itemMap[artistId]]);
|
||||
return artistIds.reduce((acc: Artist[], artistId) => {
|
||||
const artist = allArtists[itemMap[artistId]];
|
||||
|
||||
if (artist) {
|
||||
acc.push(artist);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createProfileInUseSelector(profileProp) {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
createAllArtistSelector(),
|
||||
(state) => state.settings.importLists.items,
|
||||
(id, artist, lists) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.some(artist, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createProfileInUseSelector;
|
|
@ -0,0 +1,25 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createProfileInUseSelector(profileProp: string) {
|
||||
return createSelector(
|
||||
(_: AppState, { id }: { id: number }) => id,
|
||||
createAllArtistSelector(),
|
||||
(state: AppState) => state.settings.importLists.items,
|
||||
(id, artists, lists) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
artists.some((a) => a[profileProp as keyof Artist] === id) ||
|
||||
lists.some((list) => list[profileProp as keyof ImportList] === id)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createProfileInUseSelector;
|
|
@ -1,26 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
export function createQualityProfileSelectorForHook(qualityProfileId) {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => {
|
||||
return profile.id === qualityProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createQualityProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { qualityProfileId }) => qualityProfileId,
|
||||
(state) => state.settings.qualityProfiles.items,
|
||||
(qualityProfileId, qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => {
|
||||
return profile.id === qualityProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createQualityProfileSelector;
|
|
@ -0,0 +1,24 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export function createQualityProfileSelectorForHook(qualityProfileId: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createQualityProfileSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { qualityProfileId }: { qualityProfileId: number }) =>
|
||||
qualityProfileId,
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfileId, qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createQualityProfileSelector;
|
|
@ -1,21 +1,16 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createQueueItemSelector() {
|
||||
return createSelector(
|
||||
(state, { albumId }) => albumId,
|
||||
(state) => state.queue.details.items,
|
||||
(_: AppState, { albumId }: { albumId: number }) => albumId,
|
||||
(state: AppState) => state.queue.details.items,
|
||||
(albumId, details) => {
|
||||
if (!albumId || !details) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return details.find((item) => {
|
||||
if (item.album) {
|
||||
return item.album.id === albumId;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return details.find((item) => item.albumId === albumId);
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createSystemStatusSelector() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
(state: AppState) => state.system.status,
|
||||
(status) => {
|
||||
return status.item;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createTagDetailsSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.tags.details.items,
|
||||
(_: AppState, { id }: { id: number }) => id,
|
||||
(state: AppState) => state.tags.details.items,
|
||||
(id, tagDetails) => {
|
||||
return tagDetails.find((t) => t.id === id);
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createTagsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.tags.items,
|
||||
(state: AppState) => state.tags.items,
|
||||
(tags) => {
|
||||
return tags;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createTrackFileSelector() {
|
||||
return createSelector(
|
||||
(state, { trackFileId }) => trackFileId,
|
||||
(state) => state.trackFiles,
|
||||
(_: AppState, { trackFileId }: { trackFileId: number }) => trackFileId,
|
||||
(state: AppState) => state.trackFiles,
|
||||
(trackFileId, trackFiles) => {
|
||||
if (!trackFileId) {
|
||||
return;
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createUISettingsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui,
|
||||
(state: AppState) => state.settings.ui,
|
||||
(ui) => {
|
||||
return ui.item;
|
||||
}
|
|
@ -2,7 +2,7 @@ import * as dark from './dark';
|
|||
import * as light from './light';
|
||||
|
||||
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const auto = defaultDark ? { ...dark } : { ...light };
|
||||
const auto = defaultDark ? dark : light;
|
||||
|
||||
export default {
|
||||
auto,
|
||||
|
|
|
@ -2,7 +2,6 @@ module.exports = {
|
|||
// Families
|
||||
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
|
||||
passwordFamily: 'text-security-disc',
|
||||
|
||||
// Sizes
|
||||
extraSmallFontSize: '11px',
|
||||
|
|
|
@ -6,6 +6,22 @@ import createMultiArtistsSelector from 'Store/Selectors/createMultiArtistsSelect
|
|||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRowNameCell.css';
|
||||
|
||||
function formatTitles(titles: string[]) {
|
||||
if (!titles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (titles.length > 11) {
|
||||
return (
|
||||
<span title={titles.join(', ')}>
|
||||
{titles.slice(0, 10).join(', ')}, {titles.length - 10} more
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{titles.join(', ')}</span>;
|
||||
}
|
||||
|
||||
export interface QueuedTaskRowNameCellProps {
|
||||
commandName: string;
|
||||
body: CommandBody;
|
||||
|
@ -32,7 +48,7 @@ export default function QueuedTaskRowNameCell(
|
|||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
{sortedArtists.length ? (
|
||||
<span> - {sortedArtists.map((a) => a.artistName).join(', ')}</span>
|
||||
<span> - {formatTitles(sortedArtists.map((a) => a.artistName))}</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
|
|
|
@ -57,8 +57,8 @@
|
|||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f5f7fa;
|
||||
color: #656565;
|
||||
background-color: var(--pageBackground);
|
||||
color: var(--textColor);
|
||||
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
@ -88,14 +88,14 @@
|
|||
padding: 10px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
background-color: #3a3f51;
|
||||
background-color: var(--themeDarkColor);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 20px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fff;
|
||||
background-color: var(--panelBackground);
|
||||
}
|
||||
|
||||
.sign-in {
|
||||
|
@ -112,16 +112,18 @@
|
|||
padding: 6px 16px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 1px solid #dde6e9;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
|
||||
color: var(--textColor);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: 0;
|
||||
border-color: #66afe9;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px rgba(102, 175, 233, 0.6);
|
||||
border-color: var(--inputFocusBorderColor);
|
||||
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
|
||||
0 0 8px var(--inputFocusBoxShadowColor);
|
||||
}
|
||||
|
||||
.button {
|
||||
|
@ -130,10 +132,10 @@
|
|||
padding: 10px 0;
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
border-color: #1d563d;
|
||||
border-color: var(--primaryBorderColor);
|
||||
border-radius: 4px;
|
||||
background-color: #0b8750;
|
||||
color: #fff;
|
||||
background-color: var(--primaryBackgroundColor);
|
||||
color: var(--white);
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
@ -141,9 +143,9 @@
|
|||
}
|
||||
|
||||
.button:hover {
|
||||
border-color: #1D563D;
|
||||
background-color: #097948;
|
||||
color: #fff;
|
||||
border-color: var(--primaryHoverBorderColor);
|
||||
background-color: var(--primaryHoverBackgroundColor);
|
||||
color: var(--white);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -165,24 +167,24 @@
|
|||
|
||||
.forgot-password {
|
||||
margin-left: auto;
|
||||
color: #909fa7;
|
||||
color: var(--forgotPasswordColor);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.forgot-password:focus,
|
||||
.forgot-password:hover {
|
||||
color: #748690;
|
||||
color: var(--forgotPasswordAltColor);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.forgot-password:visited {
|
||||
color: #748690;
|
||||
color: var(--forgotPasswordAltColor);
|
||||
}
|
||||
|
||||
.login-failed {
|
||||
margin-top: 20px;
|
||||
color: #f05050;
|
||||
color: var(--failedColor);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
@ -291,5 +293,59 @@
|
|||
|
||||
loginFailedDiv.classList.remove("hidden");
|
||||
}
|
||||
|
||||
var light = {
|
||||
white: '#fff',
|
||||
pageBackground: '#f5f7fa',
|
||||
textColor: '#515253',
|
||||
themeDarkColor: '#3a3f51',
|
||||
panelBackground: '#fff',
|
||||
inputBackgroundColor: '#fff',
|
||||
inputBorderColor: '#dde6e9',
|
||||
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||
inputFocusBorderColor: '#66afe9',
|
||||
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||
primaryBackgroundColor: '#0b8750',
|
||||
primaryBorderColor: '#1d563d',
|
||||
primaryHoverBackgroundColor: '#097948',
|
||||
primaryHoverBorderColor: '#1D563D',
|
||||
failedColor: '#f05050',
|
||||
forgotPasswordColor: '#909fa7',
|
||||
forgotPasswordAltColor: '#748690'
|
||||
};
|
||||
|
||||
var dark = {
|
||||
white: '#fff',
|
||||
pageBackground: '#202020',
|
||||
textColor: '#ccc',
|
||||
themeDarkColor: '#494949',
|
||||
panelBackground: '#111',
|
||||
inputBackgroundColor: '#333',
|
||||
inputBorderColor: '#dde6e9',
|
||||
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||
inputFocusBorderColor: '#66afe9',
|
||||
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||
primaryBackgroundColor: '#0b8750',
|
||||
primaryBorderColor: '#1d563d',
|
||||
primaryHoverBackgroundColor: '#097948',
|
||||
primaryHoverBorderColor: '#1D563D',
|
||||
failedColor: '#f05050',
|
||||
forgotPasswordColor: '#737d83',
|
||||
forgotPasswordAltColor: '#546067'
|
||||
};
|
||||
|
||||
var theme = "_THEME_";
|
||||
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
|
||||
dark :
|
||||
light;
|
||||
|
||||
Object.entries(finalTheme).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--${key}`,
|
||||
value
|
||||
);
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
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;
|
||||
runtimeName: string;
|
||||
runtimeVersion: string;
|
||||
sqliteVersion: string;
|
||||
startTime: string;
|
||||
startupPath: string;
|
||||
urlBase: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export default SystemStatus;
|
|
@ -1,4 +1,5 @@
|
|||
export interface UiSettings {
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
|
|
42
package.json
42
package.json
|
@ -34,9 +34,9 @@
|
|||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.16.14",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@types/node": "18.19.31",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"ansi-colors": "4.1.3",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
|
@ -87,47 +87,47 @@
|
|||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "4.9.5"
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.11",
|
||||
"@babel/eslint-parser": "7.22.11",
|
||||
"@babel/plugin-proposal-export-default-from": "7.22.5",
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.22.14",
|
||||
"@babel/preset-react": "7.22.5",
|
||||
"@babel/preset-typescript": "7.22.11",
|
||||
"@types/lodash": "4.14.197",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-lazyload": "3.2.0",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.14.1",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/redux-actions": "2.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.7",
|
||||
"@typescript-eslint/parser": "5.59.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.32.1",
|
||||
"core-js": "3.37.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-json": "3.1.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.23",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
|
|
|
@ -101,8 +101,8 @@
|
|||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||
</ItemGroup>
|
||||
|
@ -147,16 +147,46 @@
|
|||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<!--
|
||||
Set architecture to RuntimeInformation.ProcessArchitecture if not specified -->
|
||||
<Choose>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">
|
||||
<PropertyGroup>
|
||||
<Architecture>x64</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'">
|
||||
<PropertyGroup>
|
||||
<Architecture>x86</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">
|
||||
<PropertyGroup>
|
||||
<Architecture>arm64</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'">
|
||||
<PropertyGroup>
|
||||
<Architecture>arm</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup>
|
||||
<Architecture></Architecture>
|
||||
</PropertyGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
||||
'$(RuntimeIdentifier)' == ''">
|
||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
||||
'$(RuntimeIdentifier)' == ''">
|
||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
||||
|
|
|
@ -132,7 +132,8 @@ namespace Lidarr.Api.V1.Indexers
|
|||
MagnetUrl = resource.MagnetUrl,
|
||||
InfoHash = resource.InfoHash,
|
||||
Seeders = resource.Seeders,
|
||||
Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null
|
||||
Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null,
|
||||
IndexerFlags = (IndexerFlags)resource.IndexerFlags
|
||||
};
|
||||
}
|
||||
else
|
||||
|
|
|
@ -204,10 +204,10 @@ namespace Lidarr.Api.V1
|
|||
[SkipValidation(true, false)]
|
||||
[HttpPost("test")]
|
||||
[Consumes("application/json")]
|
||||
public object Test([FromBody] TProviderResource providerResource)
|
||||
public object Test([FromBody] TProviderResource providerResource, [FromQuery] bool forceTest = false)
|
||||
{
|
||||
var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null;
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, true, true, true);
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceTest, true);
|
||||
|
||||
Test(providerDefinition, true);
|
||||
|
||||
|
|
|
@ -2302,6 +2302,16 @@
|
|||
"tags": [
|
||||
"DownloadClient"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -3208,6 +3218,16 @@
|
|||
"tags": [
|
||||
"ImportList"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -3705,6 +3725,16 @@
|
|||
"tags": [
|
||||
"Indexer"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -4584,6 +4614,16 @@
|
|||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -5516,6 +5556,16 @@
|
|||
"tags": [
|
||||
"Notification"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -5620,6 +5670,23 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"head": {
|
||||
"tags": [
|
||||
"Ping"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PingResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/qualitydefinition/{id}": {
|
||||
|
|
|
@ -161,7 +161,7 @@ namespace Lidarr.Http.ClientSchema
|
|||
field.Hidden = fieldAttribute.Hidden.ToString().FirstCharToLower();
|
||||
}
|
||||
|
||||
if (fieldAttribute.Type is FieldType.Number && propertyInfo.PropertyType == typeof(double))
|
||||
if (fieldAttribute.Type is FieldType.Number && (propertyInfo.PropertyType == typeof(double) || propertyInfo.PropertyType == typeof(double?)))
|
||||
{
|
||||
field.IsFloat = true;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ namespace Lidarr.Http.Frontend.Mappers
|
|||
return stream;
|
||||
}
|
||||
|
||||
protected string GetHtmlText()
|
||||
protected virtual string GetHtmlText()
|
||||
{
|
||||
if (RuntimeInfo.IsProduction && _generatedContent != null)
|
||||
{
|
||||
|
|
|
@ -9,6 +9,8 @@ namespace Lidarr.Http.Frontend.Mappers
|
|||
{
|
||||
public class LoginHtmlMapper : HtmlMapperBase
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
|
||||
IDiskProvider diskProvider,
|
||||
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
|
||||
|
@ -16,6 +18,7 @@ namespace Lidarr.Http.Frontend.Mappers
|
|||
Logger logger)
|
||||
: base(diskProvider, cacheBreakProviderFactory, logger)
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html");
|
||||
UrlBase = configFileProvider.UrlBase;
|
||||
}
|
||||
|
@ -29,5 +32,15 @@ namespace Lidarr.Http.Frontend.Mappers
|
|||
{
|
||||
return resourceUrl.StartsWith("/login");
|
||||
}
|
||||
|
||||
protected override string GetHtmlText()
|
||||
{
|
||||
var html = base.GetHtmlText();
|
||||
var theme = _configFileProvider.Theme;
|
||||
|
||||
html = html.Replace("_THEME_", theme);
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace Lidarr.Http.Ping
|
|||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("/ping")]
|
||||
[HttpHead("/ping")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<PingResource> GetStatus()
|
||||
{
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Test.Common;
|
||||
|
@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test
|
|||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
|
||||
.Callback<string, string>((p, t) => _configFileContents = t);
|
||||
|
||||
Mocker.GetMock<IOptions<AuthOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new AuthOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<AppOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new AppOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<ServerOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new ServerOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<LogOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new LogOptions());
|
||||
|
||||
Mocker.GetMock<IOptions<UpdateOptions>>()
|
||||
.Setup(v => v.Value)
|
||||
.Returns(new UpdateOptions());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -19,6 +19,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
|
||||
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||
[TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||
|
||||
// Indexer Responses
|
||||
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||
|
|
|
@ -10,6 +10,7 @@ using NUnit.Framework;
|
|||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
|
@ -33,6 +34,11 @@ namespace NzbDrone.Common.Test
|
|||
|
||||
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object);
|
||||
|
||||
var serviceProvider = container.GetServiceProvider();
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ using System.Net.Sockets;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
|
@ -30,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
||||
private readonly ICached<CredentialCache> _credentialCache;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
||||
ICreateManagedWebProxy createManagedWebProxy,
|
||||
ICertificateValidationService certificateValidationService,
|
||||
IUserAgentBuilder userAgentBuilder,
|
||||
ICacheManager cacheManager)
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_proxySettingsProvider = proxySettingsProvider;
|
||||
_createManagedWebProxy = createManagedWebProxy;
|
||||
|
@ -43,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
|
||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
||||
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||
|
@ -251,19 +257,27 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||
}
|
||||
|
||||
private static bool HasRoutableIPv4Address()
|
||||
private bool HasRoutableIPv4Address()
|
||||
{
|
||||
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
try
|
||||
{
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
||||
|
@ -287,7 +301,9 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
catch
|
||||
{
|
||||
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||
useIPv6 = !HasRoutableIPv4Address();
|
||||
var routableIPv4 = HasRoutableIPv4Address();
|
||||
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
|
||||
useIPv6 = !routableIPv4;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class AppOptions
|
||||
{
|
||||
public string InstanceName { get; set; }
|
||||
public string Theme { get; set; }
|
||||
public bool? LaunchBrowser { get; set; }
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class AuthOptions
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public bool? Enabled { get; set; }
|
||||
public string Method { get; set; }
|
||||
public string Required { get; set; }
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class LogOptions
|
||||
{
|
||||
public string Level { get; set; }
|
||||
public bool? FilterSentryEvents { get; set; }
|
||||
public int? Rotate { get; set; }
|
||||
public bool? Sql { get; set; }
|
||||
public string ConsoleLevel { get; set; }
|
||||
public bool? AnalyticsEnabled { get; set; }
|
||||
public string SyslogServer { get; set; }
|
||||
public int? SyslogPort { get; set; }
|
||||
public string SyslogLevel { get; set; }
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class ServerOptions
|
||||
{
|
||||
public string UrlBase { get; set; }
|
||||
public string BindAddress { get; set; }
|
||||
public int? Port { get; set; }
|
||||
public bool? EnableSsl { get; set; }
|
||||
public int? SslPort { get; set; }
|
||||
public string SslCertPath { get; set; }
|
||||
public string SslCertPassword { get; set; }
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace NzbDrone.Common.Options;
|
||||
|
||||
public class UpdateOptions
|
||||
{
|
||||
public string Mechanism { get; set; }
|
||||
public bool? Automatically { get; set; }
|
||||
public string ScriptPath { get; set; }
|
||||
public string Branch { get; set; }
|
||||
}
|
|
@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
Subject.Definition.Settings.As<QBittorrentSettings>().RecentMusicPriority = (int)QBittorrentPriority.First;
|
||||
}
|
||||
|
||||
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
|
||||
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, int maxInactiveSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
|
||||
{
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||
|
@ -118,7 +118,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
MaxRatio = maxRatio,
|
||||
MaxRatioEnabled = maxRatio >= 0,
|
||||
MaxSeedingTime = maxSeedingTime,
|
||||
MaxSeedingTimeEnabled = maxSeedingTime >= 0
|
||||
MaxSeedingTimeEnabled = maxSeedingTime >= 0,
|
||||
MaxInactiveSeedingTime = maxInactiveSeedingTime,
|
||||
MaxInactiveSeedingTimeEnabled = maxInactiveSeedingTime >= 0
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -176,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
VerifyWarning(item);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void paused_item_should_have_required_properties()
|
||||
[TestCase("pausedDL")]
|
||||
[TestCase("stoppedDL")]
|
||||
public void paused_item_should_have_required_properties(string state)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
|
@ -186,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = "pausedDL",
|
||||
State = state,
|
||||
Label = "",
|
||||
SavePath = ""
|
||||
};
|
||||
|
@ -198,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
[TestCase("queuedUP")]
|
||||
[TestCase("uploading")]
|
||||
[TestCase("stalledUP")]
|
||||
|
@ -415,8 +419,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
item.RemainingTime.Should().NotHaveValue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void api_261_should_use_content_path()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void api_261_should_use_content_path(string state)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
|
@ -425,7 +430,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = "pausedUP",
|
||||
State = state,
|
||||
Label = "",
|
||||
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
||||
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
||||
|
@ -609,7 +614,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
float ratio = 0.1f,
|
||||
float ratioLimit = -2,
|
||||
int seedingTime = 1,
|
||||
int seedingTimeLimit = -2)
|
||||
int seedingTimeLimit = -2,
|
||||
int inactiveSeedingTimeLimit = -2,
|
||||
long lastActivity = -1)
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
|
@ -623,7 +630,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
SavePath = "",
|
||||
Ratio = ratio,
|
||||
RatioLimit = ratioLimit,
|
||||
SeedingTimeLimit = seedingTimeLimit
|
||||
SeedingTimeLimit = seedingTimeLimit,
|
||||
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit,
|
||||
LastActivity = lastActivity == -1 ? DateTimeOffset.UtcNow.ToUnixTimeSeconds() : lastActivity
|
||||
};
|
||||
|
||||
GivenTorrents(new List<QBittorrentTorrent>() { torrent });
|
||||
|
@ -649,44 +658,48 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(0.2f);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
|
||||
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
|
@ -704,33 +717,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 40);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
|
@ -738,21 +754,82 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
|
||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_inactive_seedingtime_reached_and_not_paused()
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, 20);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent("uploading", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_fetch_details_twice()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
item.CanMoveFiles.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, 20);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
|
||||
|
||||
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_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeTrue();
|
||||
item.CanMoveFiles.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_not_fetch_details_twice(string state)
|
||||
{
|
||||
GivenGlobalSeedLimits(-1, 30);
|
||||
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
||||
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
|
||||
|
||||
var item = Subject.GetItems().Single();
|
||||
item.CanBeRemoved.Should().BeFalse();
|
||||
|
@ -764,8 +841,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_category_from_the_category_if_set()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_get_category_from_the_category_if_set(string state)
|
||||
{
|
||||
const string category = "music-lidarr";
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
|
@ -777,7 +855,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "pausedUP",
|
||||
State = state,
|
||||
Category = category,
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
|
@ -789,8 +867,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
item.Category.Should().Be(category);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_category_from_the_label_if_the_category_is_not_available()
|
||||
[TestCase("pausedUP")]
|
||||
[TestCase("stoppedUP")]
|
||||
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
|
||||
{
|
||||
const string category = "music-lidarr";
|
||||
GivenGlobalSeedLimits(1.0f);
|
||||
|
@ -802,7 +881,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
Size = 1000,
|
||||
Progress = 1.0,
|
||||
Eta = 8640000,
|
||||
State = "pausedUP",
|
||||
State = state,
|
||||
Label = category,
|
||||
SavePath = "",
|
||||
Ratio = 1.0f
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.Framework
|
|||
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
||||
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
||||
Mocker.SetConstant<ILidarrCloudRequestBuilder>(new LidarrCloudRequestBuilder());
|
||||
Mocker.SetConstant<IMetadataRequestBuilder>(Mocker.Resolve<MetadataRequestBuilder>());
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Tags;
|
||||
|
@ -46,5 +49,35 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
|||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_used_auto_tagging_tag_specification_tags()
|
||||
{
|
||||
var tags = Builder<Tag>
|
||||
.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.BuildList();
|
||||
Db.InsertMany(tags);
|
||||
|
||||
var autoTags = Builder<AutoTag>.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.With(x => x.Specifications = new List<IAutoTaggingSpecification>
|
||||
{
|
||||
new TagSpecification
|
||||
{
|
||||
Name = "Test",
|
||||
Value = tags[0].Id
|
||||
}
|
||||
})
|
||||
.BuildList();
|
||||
|
||||
Mocker.GetMock<IAutoTaggingRepository>().Setup(s => s.All())
|
||||
.Returns(autoTags);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-03-20 00:00:00Z")]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-06-20 00:00:00Z")]
|
||||
public void map_artist_should_work()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
@ -159,7 +159,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-03-20 00:00:00Z")]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-06-20 00:00:00Z")]
|
||||
public void map_album_should_work()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
|
|
@ -86,7 +86,8 @@ namespace NzbDrone.Core.Annotations
|
|||
TagSelect,
|
||||
RootFolder,
|
||||
QualityProfile,
|
||||
MetadataProfile
|
||||
MetadataProfile,
|
||||
ArtistTag
|
||||
}
|
||||
|
||||
public enum HiddenType
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
{
|
||||
public class TagSpecificationValidator : AbstractValidator<TagSpecification>
|
||||
{
|
||||
public TagSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
public class TagSpecification : AutoTaggingSpecificationBase
|
||||
{
|
||||
private static readonly TagSpecificationValidator Validator = new ();
|
||||
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Tag";
|
||||
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationTag", Type = FieldType.ArtistTag)]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Artist artist)
|
||||
{
|
||||
return artist.Tags.Contains(Value);
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ using NzbDrone.Common.Cache;
|
|||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
@ -70,6 +71,11 @@ namespace NzbDrone.Core.Configuration
|
|||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly ICached<string> _cache;
|
||||
private readonly PostgresOptions _postgresOptions;
|
||||
private readonly AuthOptions _authOptions;
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly ServerOptions _serverOptions;
|
||||
private readonly UpdateOptions _updateOptions;
|
||||
private readonly LogOptions _logOptions;
|
||||
|
||||
private readonly string _configFile;
|
||||
|
||||
|
@ -79,13 +85,23 @@ namespace NzbDrone.Core.Configuration
|
|||
ICacheManager cacheManager,
|
||||
IEventAggregator eventAggregator,
|
||||
IDiskProvider diskProvider,
|
||||
IOptions<PostgresOptions> postgresOptions)
|
||||
IOptions<PostgresOptions> postgresOptions,
|
||||
IOptions<AuthOptions> authOptions,
|
||||
IOptions<AppOptions> appOptions,
|
||||
IOptions<ServerOptions> serverOptions,
|
||||
IOptions<UpdateOptions> updateOptions,
|
||||
IOptions<LogOptions> logOptions)
|
||||
{
|
||||
_cache = cacheManager.GetCache<string>(GetType());
|
||||
_eventAggregator = eventAggregator;
|
||||
_diskProvider = diskProvider;
|
||||
_configFile = appFolderInfo.GetConfigPath();
|
||||
_postgresOptions = postgresOptions.Value;
|
||||
_authOptions = authOptions.Value;
|
||||
_appOptions = appOptions.Value;
|
||||
_serverOptions = serverOptions.Value;
|
||||
_updateOptions = updateOptions.Value;
|
||||
_logOptions = logOptions.Value;
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetConfigDictionary()
|
||||
|
@ -141,7 +157,7 @@ namespace NzbDrone.Core.Configuration
|
|||
{
|
||||
const string defaultValue = "*";
|
||||
|
||||
var bindAddress = GetValue("BindAddress", defaultValue);
|
||||
var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue);
|
||||
if (string.IsNullOrWhiteSpace(bindAddress))
|
||||
{
|
||||
return defaultValue;
|
||||
|
@ -151,19 +167,19 @@ namespace NzbDrone.Core.Configuration
|
|||
}
|
||||
}
|
||||
|
||||
public int Port => GetValueInt("Port", 8686);
|
||||
public int Port => _serverOptions.Port ?? GetValueInt("Port", 8686);
|
||||
|
||||
public int SslPort => GetValueInt("SslPort", 6868);
|
||||
public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", 6868);
|
||||
|
||||
public bool EnableSsl => GetValueBoolean("EnableSsl", false);
|
||||
public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false);
|
||||
|
||||
public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true);
|
||||
public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true);
|
||||
|
||||
public string ApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
var apiKey = GetValue("ApiKey", GenerateApiKey());
|
||||
var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey());
|
||||
|
||||
if (apiKey.IsNullOrWhiteSpace())
|
||||
{
|
||||
|
@ -179,7 +195,7 @@ namespace NzbDrone.Core.Configuration
|
|||
{
|
||||
get
|
||||
{
|
||||
var enabled = GetValueBoolean("AuthenticationEnabled", false, false);
|
||||
var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false);
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
|
@ -187,37 +203,44 @@ namespace NzbDrone.Core.Configuration
|
|||
return AuthenticationType.Basic;
|
||||
}
|
||||
|
||||
return GetValueEnum("AuthenticationMethod", AuthenticationType.None);
|
||||
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("AuthenticationMethod", AuthenticationType.None);
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||
public AuthenticationRequiredType AuthenticationRequired =>
|
||||
Enum.TryParse<AuthenticationRequiredType>(_authOptions.Required, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||
|
||||
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||
public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||
|
||||
public string Branch => GetValue("Branch", "master").ToLowerInvariant();
|
||||
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "master").ToLowerInvariant();
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info");
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
|
||||
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
|
||||
|
||||
public string Theme => GetValue("Theme", "auto", persist: false);
|
||||
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
|
||||
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
|
||||
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
|
||||
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "lidarr-main", persist: false);
|
||||
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "lidarr-log", persist: false);
|
||||
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
|
||||
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
||||
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
public string SslCertPath => GetValue("SslCertPath", "");
|
||||
public string SslCertPassword => GetValue("SslCertPassword", "");
|
||||
|
||||
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
|
||||
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
|
||||
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
||||
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
||||
|
||||
public string UrlBase
|
||||
{
|
||||
get
|
||||
{
|
||||
var urlBase = GetValue("UrlBase", "").Trim('/');
|
||||
var urlBase = _serverOptions.UrlBase ?? GetValue("UrlBase", "").Trim('/');
|
||||
|
||||
if (urlBase.IsNullOrWhiteSpace())
|
||||
{
|
||||
|
@ -229,19 +252,22 @@ namespace NzbDrone.Core.Configuration
|
|||
}
|
||||
|
||||
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
|
||||
public string InstanceName => GetValue("InstanceName", BuildInfo.AppName);
|
||||
public string InstanceName => _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
|
||||
|
||||
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
|
||||
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false);
|
||||
|
||||
public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
|
||||
public UpdateMechanism UpdateMechanism =>
|
||||
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
|
||||
? enumValue
|
||||
: GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
|
||||
|
||||
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
|
||||
public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false);
|
||||
|
||||
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
|
||||
public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false);
|
||||
|
||||
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
|
||||
public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false);
|
||||
|
||||
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, false).ToLowerInvariant();
|
||||
public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
|
||||
|
||||
public int GetValueInt(string key, int defaultValue, bool persist = true)
|
||||
{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue