1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-29 18:15:37 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
nitsua
315c2e1b88 Add support for template switching using Theme Park templates 2021-11-12 07:58:12 -05:00
373 changed files with 3859 additions and 4744 deletions

View File

@@ -1,4 +1,5 @@
name: Bug Report
title: "[BUG]: "
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
@@ -63,11 +64,12 @@ body:
required: true
- type: textarea
attributes:
label: Trace Logs?
label: Anything else?
description: |
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
Links? References? Anything that will give us more context about the issue you are encountering!
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true

View File

@@ -1,4 +1,5 @@
name: Feature Request
title: "[FEAT]: "
description: 'Suggest an idea for Radarr'
labels: ['Type: Feature Request', 'Status: Needs Triage']
body:

View File

@@ -1,21 +1,19 @@
# Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Translated](https://translate.servarr.com/widgets/radarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)
[![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors)
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
## Major Features Include
## Major Features Include:
* Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
* Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet
@@ -23,60 +21,53 @@ Note that only one type of a given movie is supported. If you want both an 4k ve
* Automatically importing downloaded movies
* Recognizing Special Editions, Director's Cut, etc.
* Identifying releases with hardcoded subs
* Identifying releases with AKA movie names
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
* Full integration with Kodi and Plex (notifications, library updates)
* QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported
* Full integration with Kodi, Plex (notification, library update)
* A beautiful UI
* Importing Metadata such as trailers or subtitles
* Adding metadata such as posters and information for Kodi and others to use
* Advanced customization for profiles, such that Radarr will always download the copy you want
* A beautiful UI
## Support
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
Note: GitHub Issues are for Bugs and Feature Requests Only
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Radarr/Radarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
## Contributors & Developers
[API Documentation](https://radarr.video/docs/api/)
This project exists thanks to all the people who contribute.
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/radarr/contributing)
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Radarr/Radarr/graphs/contributors"><img src="https://opencollective.com/Radarr/contributors.svg?width=890&button=false" /></a>
[![Contributors List](https://opencollective.com/Radarr/contributors.svg?width=890&button=false)](https://github.com/Radarr/Radarr/graphs/contributors)
## Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Radarr#backer)
[![Backers List](https://opencollective.com/Radarr/backers.svg?width=890)](https://opencollective.com/Radarr#backer)
<img src="https://opencollective.com/Radarr/backers.svg?width=890"></a>
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Radarr#sponsor)
[![Sponsors List](https://opencollective.com/Radarr/sponsors.svg?width=890)](https://opencollective.com/Radarr#sponsor)
<img src="https://opencollective.com/Radarr/sponsors.svg?width=890"></a>
## Mega Sponsors
[![Mega Sponsors List](https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/Radarr#mega-sponsor)
<img src="https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890"></a>
## JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022
* Copyright 2010-2021

View File

@@ -13,7 +13,7 @@ variables:
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.100'
dotnetVersion: '5.0.401'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger:
@@ -111,23 +111,23 @@ stages:
artifact: '$(osName)Backend'
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
- publish: '$(testsFolder)/net5.0/win-x64/publish'
artifact: WindowsCoreTests
displayName: Publish Windows Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
artifact: LinuxCoreTests
displayName: Publish Linux Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
artifact: MacCoreTests
displayName: Publish MacOS Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
@@ -203,12 +203,12 @@ stages:
- bash: ./build.sh --packages
displayName: Create Packages
- bash: |
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x86
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x86
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create .NET Core Windows installer
- bash: |
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x64
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x64
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create .NET Core Windows installer
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller'
@@ -251,44 +251,29 @@ stages:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
- task: ArchiveFiles@2
displayName: Create Windows x86 Core zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
- task: ArchiveFiles@2
displayName: Create MacOS x64 Core app
displayName: Create MacOS Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
- task: ArchiveFiles@2
displayName: Create MacOS x64 Core tar
displayName: Create MacOS Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS arm64 Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS arm64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
- task: ArchiveFiles@2
displayName: Create Linux Core tar
inputs:
@@ -296,7 +281,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
- task: ArchiveFiles@2
displayName: Create Linux Musl Core tar
inputs:
@@ -304,7 +289,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
- task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar
inputs:
@@ -312,15 +297,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2
displayName: Create ARM32 Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
- task: ArchiveFiles@2
displayName: Create ARM64 Linux Core tar
inputs:
@@ -328,7 +305,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
- task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar
inputs:
@@ -336,7 +313,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
- task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar
inputs:
@@ -344,7 +321,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'Packages'
displayName: Publish Packages
@@ -889,8 +866,8 @@ stages:
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
./build.sh --backend -f net5.0 -r win-x64
TEST_DIR=_tests/net5.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')

View File

@@ -129,7 +129,7 @@ PackageLinux()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net5.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -140,13 +140,12 @@ PackageLinux()
PackageMacOS()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating MacOS Package for $framework $runtime"
ProgressStart "Creating MacOS Package for $framework"
local folder=$artifactsFolder/$runtime/$framework/Radarr
local folder=$artifactsFolder/macos/$framework/Radarr
PackageFiles "$folder" "$framework" "$runtime"
PackageFiles "$folder" "$framework" "osx-x64"
echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.*
@@ -157,7 +156,7 @@ PackageMacOS()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net5.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -168,11 +167,10 @@ PackageMacOS()
PackageMacOSApp()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating macOS App Package for $framework $runtime"
ProgressStart "Creating macOS App Package for $framework"
local folder="$artifactsFolder/$runtime-app/$framework"
local folder=$artifactsFolder/macos-app/$framework
rm -rf $folder
mkdir -p $folder
@@ -180,7 +178,7 @@ PackageMacOSApp()
mkdir -p $folder/Radarr.app/Contents/MacOS
echo "Copying Binaries"
cp -r $artifactsFolder/$runtime/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
cp -r $artifactsFolder/macos/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
echo "Removing Update Folder"
rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update
@@ -227,8 +225,8 @@ Package()
PackageWindows "$framework" "$runtime"
;;
osx)
PackageMacOS "$framework" "$runtime"
PackageMacOSApp "$framework" "$runtime"
PackageMacOS "$framework"
PackageMacOSApp "$framework"
;;
esac
}
@@ -328,14 +326,14 @@ then
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
PackageTests "net6.0" "win-x64"
PackageTests "net6.0" "win-x86"
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
PackageTests "net5.0" "win-x64"
PackageTests "net5.0" "win-x86"
PackageTests "net5.0" "linux-x64"
PackageTests "net5.0" "linux-musl-x64"
PackageTests "net5.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ];
then
PackageTests "net6.0" "freebsd-x64"
PackageTests "net5.0" "freebsd-x64"
fi
else
PackageTests "$FRAMEWORK" "$RID"
@@ -364,19 +362,17 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
Package "net6.0" "win-x64"
Package "net6.0" "win-x86"
Package "net6.0" "linux-x64"
Package "net6.0" "linux-musl-x64"
Package "net6.0" "linux-arm64"
Package "net6.0" "linux-musl-arm64"
Package "net6.0" "linux-arm"
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
Package "net5.0" "win-x64"
Package "net5.0" "win-x86"
Package "net5.0" "linux-x64"
Package "net5.0" "linux-musl-x64"
Package "net5.0" "linux-arm64"
Package "net5.0" "linux-musl-arm64"
Package "net5.0" "linux-arm"
Package "net5.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ];
then
Package "net6.0" "freebsd-x64"
Package "net5.0" "freebsd-x64"
fi
else
Package "$FRAMEWORK" "$RID"

View File

@@ -4,6 +4,7 @@
],
"ignoreFiles": [
"frontend/src/Styles/scaffolding.css",
"**/Theme.Park/**/*.css",
"**/*.js"
],
"rules": {

View File

@@ -124,6 +124,20 @@ module.exports = (env) => {
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
},
// Theme.Park
{
source: 'frontend/src/Content/Theme.Park/*',
destination: path.join(distFolder, 'Content/Theme.Park')
},
{
source: 'frontend/src/Content/Theme.Park/Themes/*',
destination: path.join(distFolder, 'Content/Theme.Park/Themes')
},
{
source: 'frontend/src/Content/Theme.Park/Resources/*',
destination: path.join(distFolder, 'Content/Theme.Park/Resources')
}
]
}
@@ -243,6 +257,19 @@ module.exports = (env) => {
}
}
]
},
{
test: /\.(png)?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Theme.Park/Resources/[name].[ext]'
}
}
]
}
]
}

View File

@@ -120,7 +120,7 @@ class Blocklist extends Component {
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}

View File

@@ -161,7 +161,7 @@ class AddNewMovie extends Component {
{translate('YouCanAlsoSearch')}
</div>
<div>
<Link to="https://wiki.servarr.com/radarr/faq#why-can-i-not-add-a-new-movie-to-radarr">
<Link to="https://wiki.servarr.com/radarr/faq#why-cant-i-add-a-new-movie-to-radarr">
{translate('CantFindMovie')}
</Link>
</div>

View File

@@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react';
const theme = window.Radarr.theme;
function ThemeSelector({ children }) {
return (
<>
{
theme !== 'default' &&
<>
<link rel="stylesheet" type="text/css"
href={'/Content/Theme.Park/radarr-base.css'}
/>
<link rel="stylesheet" type="text/css"
href={`/Content/Theme.Park/Themes/${theme}.css`}
/>
</>
}
{children}
</>
);
}
ThemeSelector.propTypes = {
children: PropTypes.object.isRequired
};
export default ThemeSelector;

View File

@@ -43,15 +43,15 @@ class CalendarEvent extends Component {
const link = `/movie/${titleSlug}`;
const eventType = [];
if (inCinemas && moment(date).isSame(moment(inCinemas), 'day')) {
if (moment(date).isSame(moment(inCinemas), 'day')) {
eventType.push('Cinemas');
}
if (physicalRelease && moment(date).isSame(moment(physicalRelease), 'day')) {
if (moment(date).isSame(moment(physicalRelease), 'day')) {
eventType.push('Physical');
}
if (digitalRelease && moment(date).isSame(moment(digitalRelease), 'day')) {
if (moment(date).isSame(moment(digitalRelease), 'day')) {
eventType.push('Digital');
}

View File

@@ -16,9 +16,4 @@
color: #3a3f51;
font-size: 21px;
line-height: inherit;
&.small {
color: #909293;
font-size: 18px;
}
}

View File

@@ -1,7 +1,5 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { sizes } from 'Helpers/Props';
import styles from './FieldSet.css';
class FieldSet extends Component {
@@ -11,14 +9,13 @@ class FieldSet extends Component {
render() {
const {
size,
legend,
children
} = this.props;
return (
<fieldset className={styles.fieldSet}>
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
<legend className={styles.legend}>
{legend}
</legend>
{children}
@@ -29,13 +26,8 @@ class FieldSet extends Component {
}
FieldSet.propTypes = {
size: PropTypes.oneOf(sizes.all).isRequired,
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
children: PropTypes.node
};
FieldSet.defaultProps = {
size: sizes.MEDIUM
};
export default FieldSet;

View File

@@ -166,9 +166,7 @@ class FilterBuilderModalContent extends Component {
</div>
</div>
<div className={styles.label}>
{translate('Filters')}
</div>
<div className={styles.label}>Filters</div>
<div className={styles.rows}>
{

View File

@@ -6,7 +6,8 @@ import SelectInput from './SelectInput';
const availabilityOptions = [
{ key: 'announced', value: translate('Announced') },
{ key: 'inCinemas', value: translate('InCinemas') },
{ key: 'released', value: translate('Released') }
{ key: 'released', value: translate('Released') },
{ key: 'preDB', value: translate('PreDB') }
];
function AvailabilitySelectInput(props) {

View File

@@ -1,100 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.downloadClients,
(state, { includeAny }) => includeAny,
(state, { protocol }) => protocol,
(downloadClients, includeAny, protocolFilter) => {
const {
isFetching,
isPopulated,
error,
items
} = downloadClients;
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
return {
key: downloadClient.id,
value: downloadClient.name
};
});
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchDownloadClients: fetchDownloadClients
};
class DownloadClientSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchDownloadClients();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
DownloadClientSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired
};
DownloadClientSelectInputConnector.defaultProps = {
includeAny: false,
protocol: 'torrent'
};
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);

View File

@@ -8,7 +8,6 @@ import AvailabilitySelectInput from './AvailabilitySelectInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
@@ -74,9 +73,6 @@ function getComponent(type) {
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
case inputTypes.LANGUAGE_SELECT:
return LanguageSelectInputConnector;
@@ -117,6 +113,8 @@ function FormInputGroup(props) {
helpTexts,
helpTextWarning,
helpLink,
inlineLink,
tooltip,
pending,
errors,
warnings,
@@ -186,6 +184,9 @@ function FormInputGroup(props) {
!checkInput && helpText &&
<FormInputHelpText
text={helpText}
link={inlineLink}
tooltip={tooltip}
isWarning={!!inlineLink}
/>
}
@@ -267,6 +268,8 @@ FormInputGroup.propTypes = {
helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
inlineLink: PropTypes.string,
tooltip: PropTypes.string,
pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object)

View File

@@ -64,7 +64,6 @@ function ProviderFieldFormGroup(props) {
label,
helpText,
helpLink,
placeholder,
value,
type,
advanced,
@@ -97,7 +96,6 @@ function ProviderFieldFormGroup(props) {
label={label}
helpText={helpText}
helpLink={helpLink}
placeholder={placeholder}
value={value}
values={getSelectValues(selectOptions)}
errors={errors}
@@ -123,7 +121,6 @@ ProviderFieldFormGroup.propTypes = {
label: PropTypes.string.isRequired,
helpText: PropTypes.string,
helpLink: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.any,
type: PropTypes.string.isRequired,
advanced: PropTypes.bool.isRequired,

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: radial-gradient(ellipse at center, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--drop-down-menu-bg: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--button-color: #009688;
--button-color-hover: #12afa0;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 18, 175, 160;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #0ed2bf;
--link-color-hover: #36e7d6;
--label-text-color: #fff;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #009688; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: rgb(21, 213, 194);
--petio-spinner: invert(39%) sepia(98%) saturate(527%) hue-rotate(129deg) brightness(94%) contrast(101%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 18, 175, 160;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: radial-gradient(circle, #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(circle , #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(circle , #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(circle , #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--drop-down-menu-bg: #2d2d2d;
--button-color: #7a7a7a;
--button-color-hover: #9b9b9b;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 170, 170, 170;
--accent-color-hover: rgba(255, 255, 255, 0.45);
--link-color: #7a7a7a;
--link-color-hover: #fff;
--label-text-color: black;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #6b5; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #e5a00d;
--petio-spinner: invert(35%) sepia(12%) saturate(4%) hue-rotate(2deg) brightness(104%) contrast(86%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 255, 255, 255;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: #282a36;
--modal-bg-color: #1e2029;
--modal-header-color: #1e2029;
--modal-footer-color: #1e2029;
--drop-down-menu-bg: #1e2029;
--button-color: #bd93f9;
--button-color-hover: #ff79c6;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 80, 250, 123;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #ff79c6;
--link-color-hover: #8be9fd;
--label-text-color: #282a36;
--text:#6272a4;
--text-hover: #95adfa;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #50fa7b; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #bd93f9;
--petio-spinner: invert(79%) sepia(27%) saturate(1033%) hue-rotate(74deg) brightness(104%) contrast(96%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 80, 250, 123;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--modal-bg-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--modal-header-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--modal-footer-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--drop-down-menu-bg: linear-gradient(90deg, rgba(247,101,184,1) 0%, rgba(21, 95, 165) 100%) center center/cover no-repeat fixed;
--button-color: #f98dc9;
--button-color-hover: #ff4cb1;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 249, 141, 201;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color:rgb(255, 179, 222);
--link-color-hover: #d7fffe;
--label-text-color: #fff;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #f98dc9; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #f765b8;
--petio-spinner: invert(78%) sepia(17%) saturate(4447%) hue-rotate(290deg) brightness(109%) contrast(95%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 215,255,254;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: linear-gradient(45deg, #fb3f62 0%, #204c80 37%, #004249 97%) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(circle, #204c80 0%, #000 100%) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(circle, #204c80 0%, #000 100%) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(circle, #204c80 0%, #000 100%) center center/cover no-repeat fixed;
--drop-down-menu-bg: #204c80;
--button-color: #fb3f62;
--button-color-hover: #cd4164;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 251, 63, 98;
--accent-color-hover: rgba(var(--accent-color), .8);
--link-color: rgb(0, 255, 157);
--link-color-hover: rgba(0, 255, 157, 0.8);
--label-text-color: #282a36;
--text:#eee;
--text-hover: #fff;
--text-muted: #999;
--arr-queue-color: rgb(0, 255, 157);
--plex-poster-unwatched: #fb3f62;
--petio-spinner: invert(29%) sepia(87%) saturate(2199%) hue-rotate(331deg) brightness(115%) contrast(97%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 251, 63, 98;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: #2E3440;
--modal-bg-color: #3B4252;
--modal-header-color: #434C5E;
--modal-footer-color: #434C5E;
--drop-down-menu-bg: #3B4252;
--button-color: #79b8ca;
--button-color-hover: #6a9daf;
--button-text: #2E3440;
--button-text-hover: #D8DEE9;
--accent-color: 121, 184, 202;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #81A1C1;
--link-color-hover: #88C0D0;
--label-text-color: #222730;
--text:#D8DEE9;
--text-hover: #ECEFF4;
--text-muted: #81A1C1;
/*Specials*/
--arr-queue-color: #A3BE8C; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #D08770;
--petio-spinner: invert(83%) sepia(9%) saturate(1787%) hue-rotate(156deg) brightness(85%) contrast(83%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 121, 184, 202;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: #1f1f1f;
--modal-bg-color: #333;
--modal-header-color: #232323;
--modal-footer-color: #232323;
--drop-down-menu-bg: #1b1b1b;
--button-color: #2cabe3;
--button-color-hover: #298fbc;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 44, 171, 227;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #2cabe3;
--link-color-hover: #3cc5ff;
--label-text-color: #fff;
--text:#96a2b4;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #2cabe3; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #2cabe3;
--petio-spinner: invert(65%) sepia(83%) saturate(2026%) hue-rotate(167deg) brightness(90%) contrast(97%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 44, 171, 227;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: linear-gradient(360deg, hsl(221, 39%, 11%) 65%, hsl(215, 28%, 17%) 100%);
--modal-bg-color: #1f2937;
--modal-header-color: #1f2937;
--modal-footer-color: #1f2937;
--drop-down-menu-bg: #374151;
--button-color: #4f46e5;
--button-color-hover: #6366f1;
--button-text: #e5e7eb;
--button-text-hover: #fff;
--accent-color: 167, 139, 250;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #6366f1;
--link-color-hover: #a78bfa;
--label-text-color: #000;
--text: #d1d5db;
--text-hover: #fff;
--text-muted: #9ca3af;
/*Specials*/
--arr-queue-color: #6366f1; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #6366f1;
--petio-spinner: invert(24%) sepia(59%) saturate(3411%) hue-rotate(237deg) brightness(91%) contrast(96%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 98, 116, 145;
}

View File

@@ -0,0 +1,28 @@
:root {
--main-bg-color: url("Resources/blur-noise.png") repeat scroll 0% 0%, radial-gradient(circle at 0% 100%, rgba(54, 66, 84, 0.55) 0%, rgba(54, 66, 84, 0.043) 70%, rgba(54, 66, 84, 0) 80%), radial-gradient(circle at 100% 100%, rgba(113, 135, 153, 0.55) 0%, rgba(113, 135, 153, 0.043) 70%, rgba(113, 135, 153, 0) 80%), radial-gradient(circle at 100% 0%, rgba(54, 66, 84, 0.55) 0%, rgba(54, 66, 84, 0.043) 70%, rgba(54, 66, 84, 0) 80%), radial-gradient(circle at 0% 0%, rgba(91, 114, 135, 0.55) 0%, rgba(91, 114, 135, 0.043) 70%, rgba(91, 114, 135, 0) 80%), rgb(0, 0, 0) center center/cover no-repeat fixed;
--modal-bg-color: #1f2326;
--modal-header-color: #1f2326;
--modal-footer-color: #323232;
--drop-down-menu-bg: #191a1c;
--button-color: #cc7b19;
--button-color-hover: #e59029;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 229, 160, 13;
--accent-color-hover: #ffc107;
--link-color: #e5a00d;
--link-color-hover: #fff;
--label-text-color: #fff;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
--arr-queue-color: #27c24c;
--petio-spinner: invert(0%) sepia(0%) saturate(100%) hue-rotate(0deg) brightness(100%) contrast(100%);
--gitea-color-primary-dark-4: 255, 193, 7;
}

View File

@@ -0,0 +1,124 @@
:root {
--main-bg-color: #454545;
--modal-bg-color: #595959;
--modal-header-color: #595959;
--modal-footer-color: #595959;
--drop-down-menu-bg: #606060;
--button-color: #5899eb;
--button-color-hover: #4b91ea;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 255, 194, 48;
--accent-color-hover: rgb(255, 194, 48, .8);
--link-color: rgb(255, 194, 48);
--link-color-hover: rgb(255, 194, 48, .8);
--label-text-color: #eee;
--text: #ccc;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #5d9cec;
--side-menu-active: #333333;
--scroller-hover: #606060;
--scroller: #707070;
--border-color: #606060;
--label-color: #707070;
--label-info: #5d9cec;
--header-color: #464b51;
--side-menu-color: #595959;
}
/* HEADER */
[class*="PageHeader-header-"] {
background-color: var(--header-color);
color: #fff;
}
[class*="PageToolbar-toolbar-"] {
background-color: #707070;
color: var(--text);
}
/* SIDE MENU */
[class*="PageSidebar-sidebar-"] {
background-color: var(--side-menu-color);
color: #fff;
}
[class*=PageSidebarItem-link-]:focus {
color: rgb(var(--accent-color)) !important;
}
[class*=PageSidebarItem-isActiveLink-] {
color: var(--link-color) !important;
}
[class*=PageSidebarItem-isActiveParentLink-] {
background-color: var(--side-menu-active);
}
/* SCROLLER */
[class*=ImportSeriesSelectSeries-results-]::-webkit-scrollbar-thumb:hover,
[class*=OverlayScroller-thumb-]:hover {
background-color: var(--scroller-hover) !important;
}
[class*="OverlayScroller-thumb-"],
[class*=Scroller-scroller-]::-webkit-scrollbar-thumb {
background-color: var(--scroller) !important;
}
/* MODALS */
[class*=ModalHeader-modalHeader-],
[class*=FieldSet-legend-] {
border-bottom: 1px solid var(--border-color);
}
[class*=ModalFooter-modalFooter-] {
border-top: 1px solid var(--border-color);
}
/* LABLES */
[class*="Label-default-"] {
border-color: var(--label-color);
background-color: var(--label-color);
color: white;
}
[class*="Label-info-"]:not([class*="PageSidebarItem-status-"] [class*="Label-info-"]) {
border-color: var(--label-info);
background-color: var(--label-info);
color: #fff;
}
/* SETTINGS */
[class*=Settings-link-] {
border-bottom: 1px solid var(--border-color);
}
/* SEARCH DROP DOWN */
[class*="MovieSearchInput-containerOpen-"] [class*="MovieSearchInput-movieContainer-"] {
border: 1px solid var(--drop-down-menu-bg);
background-color: var(--drop-down-menu-bg);
box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%);
color: #e1e2e3;
}
/* SERIES PAGE */
[class*="MovieIndexPoster-controls-"] {
background-color: var(--label-color) !important;
color: #fff !important;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: radial-gradient(ellipse at center, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--drop-down-menu-bg: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--button-color: #607D8B;
--button-color-hover: #81a6b7;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 129, 166, 183;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #81a6b7;
--link-color-hover: #9adfff;
--label-text-color: #fff;
--text:#bbb;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #81a6b7; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #70aeca;
--petio-spinner: invert(50%) sepia(31%) saturate(341%) hue-rotate(155deg) brightness(88%) contrast(85%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 129, 166, 183;
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,6 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const SELECT = 'select';
export const DYNAMIC_SELECT = 'dynamicSelect';
export const TAG = 'tag';
@@ -36,7 +35,6 @@ export const all = [
PASSWORD,
PATH,
QUALITY_PROFILE_SELECT,
DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT,
LANGUAGE_SELECT,

View File

@@ -19,7 +19,6 @@ import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
@@ -41,11 +40,6 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'releaseGroup',
label: translate('ReleaseGroup'),
isVisible: true
},
{
name: 'quality',
label: translate('Quality'),
@@ -89,7 +83,6 @@ const SELECT = 'select';
const MOVIE = 'movie';
const LANGUAGE = 'language';
const QUALITY = 'quality';
const RELEASE_GROUP = 'releaseGroup';
class InteractiveImportModalContent extends Component {
@@ -209,11 +202,10 @@ class InteractiveImportModalContent extends Component {
const errorMessage = getErrorMessage(error, translate('UnableToLoadManualImportItems'));
const bulkSelectOptions = [
{ key: SELECT, value: translate('SelectDotDot'), disabled: true },
{
key: SELECT, value: translate('SelectDotDot'), disabled: true },
{ key: LANGUAGE, value: translate('SelectLanguage') },
{ key: QUALITY, value: translate('SelectQuality') },
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }
];
{ key: QUALITY, value: translate('SelectQuality') }];
if (allowMovieChange) {
bulkSelectOptions.splice(1, 0, {
@@ -380,13 +372,6 @@ class InteractiveImportModalContent extends Component {
real={false}
onModalClose={this.onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === RELEASE_GROUP}
ids={selectedIds}
releaseGroup=""
onModalClose={this.onSelectModalClose}
/>
</ModalContent>
);
}

View File

@@ -110,8 +110,7 @@ class InteractiveImportModalContentConnector extends Component {
const {
movie,
quality,
languages,
releaseGroup
languages
} = item;
if (!movie) {
@@ -133,7 +132,6 @@ class InteractiveImportModalContentConnector extends Component {
path: item.path,
folderName: item.folderName,
movieId: movie.id,
releaseGroup,
quality,
languages,
downloadId: this.props.downloadId

View File

@@ -11,7 +11,6 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -29,7 +28,6 @@ class InteractiveImportRow extends Component {
this.state = {
isSelectMovieModalOpen: false,
isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false
};
@@ -105,10 +103,6 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectMovieModalOpen: true });
}
onSelectReleaseGroupPress = () => {
this.setState({ isSelectReleaseGroupModalOpen: true });
}
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
@@ -122,11 +116,6 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed);
}
onSelectReleaseGroupModalClose = (changed) => {
this.setState({ isSelectReleaseGroupModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
@@ -148,7 +137,6 @@ class InteractiveImportRow extends Component {
movie,
quality,
languages,
releaseGroup,
size,
rejections,
isReprocessing,
@@ -159,8 +147,7 @@ class InteractiveImportRow extends Component {
const {
isSelectMovieModalOpen,
isSelectQualityModalOpen,
isSelectLanguageModalOpen,
isSelectReleaseGroupModalOpen
isSelectLanguageModalOpen
} = this.state;
const movieTitle = movie ? movie.title + ( movie.year > 0 ? ` (${movie.year})` : '') : '';
@@ -168,7 +155,6 @@ class InteractiveImportRow extends Component {
const showMoviePlaceholder = isSelected && !movie;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages && !isReprocessing;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
return (
<TableRow>
@@ -195,17 +181,6 @@ class InteractiveImportRow extends Component {
}
</TableRowCellButton>
<TableRowCellButton
title={translate('ClickToChangeReleaseGroup')}
onPress={this.onSelectReleaseGroupPress}
>
{
showReleaseGroupPlaceholder ?
<InteractiveImportRowCellPlaceholder /> :
releaseGroup
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title={translate('ClickToChangeQuality')}
@@ -293,13 +268,6 @@ class InteractiveImportRow extends Component {
onModalClose={this.onSelectMovieModalClose}
/>
<SelectReleaseGroupModal
isOpen={isSelectReleaseGroupModalOpen}
ids={[id]}
releaseGroup={releaseGroup ?? ''}
onModalClose={this.onSelectReleaseGroupModalClose}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
ids={[id]}
@@ -328,7 +296,6 @@ InteractiveImportRow.propTypes = {
movie: PropTypes.object,
quality: PropTypes.object,
languages: PropTypes.arrayOf(PropTypes.object),
releaseGroup: PropTypes.string,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
isReprocessing: PropTypes.bool,

View File

@@ -128,7 +128,7 @@ class SelectLanguageModalContent extends Component {
kind={kinds.SUCCESS}
onPress={this.onLanguageSelect}
>
{translate('SelectLanguages')}
{translate('SelectLanguges')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectReleaseGroupModalContentConnector from './SelectReleaseGroupModalContentConnector';
class SelectReleaseGroupModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectReleaseGroupModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectReleaseGroupModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModal;

View File

@@ -1,99 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class SelectReleaseGroupModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
releaseGroup
} = props;
this.state = {
releaseGroup
};
}
//
// Listeners
onReleaseGroupChange = ({ value }) => {
this.setState({ releaseGroup: value });
}
onReleaseGroupSelect = () => {
this.props.onReleaseGroupSelect(this.state);
}
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
releaseGroup
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ManualImportSetReleaseGroup')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
onChange={this.onReleaseGroupChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onReleaseGroupSelect}
>
{translate('SetReleaseGroup')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectReleaseGroupModalContent.propTypes = {
releaseGroup: PropTypes.string.isRequired,
onReleaseGroupSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModalContent;

View File

@@ -1,54 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
};
class SelectReleaseGroupModalContentConnector extends Component {
//
// Listeners
onReleaseGroupSelect = ({ releaseGroup }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
releaseGroup
});
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectReleaseGroupModalContent
{...this.props}
onReleaseGroupSelect={this.onReleaseGroupSelect}
/>
);
}
}
SelectReleaseGroupModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectReleaseGroupModalContentConnector);

View File

@@ -155,7 +155,7 @@ DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
hasFile: PropTypes.bool.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
sizeOnDisk: PropTypes.string.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -101,15 +101,6 @@ function MovieIndexSortMenu(props) {
{translate('DigitalRelease')}
</SortMenuItem>
<SortMenuItem
name="ratings"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Ratings')}
</SortMenuItem>
<SortMenuItem
name="path"
sortKey={sortKey}

View File

@@ -6,7 +6,7 @@ export function getMovieStatusDetails(status) {
let statusDetails = {
icon: icons.ANNOUNCED,
title: translate('Announced'),
message: translate('AnnouncedMsg')
message: translate('AnnoucedMsg')
};
if (status === 'deleted') {

View File

@@ -4,11 +4,6 @@
margin-right: auto;
}
.rightButtons {
justify-content: flex-end;
margin-right: auto;
}
.addSpecification {
composes: customFormat from '~./CustomFormat.css';

View File

@@ -198,25 +198,26 @@ class EditCustomFormatModalContent extends Component {
</div>
</ModalBody>
<ModalFooter>
<div className={styles.rightButtons}>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteCustomFormatPress}
>
{translate('Delete')}
</Button>
}
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteCustomFormatPress}
>
{translate('Delete')}
</Button>
}
<Button
className={styles.deleteButton}
onPress={this.onImportPress}
>
{translate('Import')}
</Button>
</div>
{
!id &&
<Button
className={styles.deleteButton}
onPress={this.onImportPress}
>
{translate('Import')}
</Button>
}
<Button
onPress={onModalClose}

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -14,7 +13,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css';
@@ -46,10 +45,7 @@ class EditDownloadClientModalContent extends Component {
implementationName,
name,
enable,
protocol,
priority,
removeCompletedDownloads,
removeFailedDownloads,
fields,
message
} = item;
@@ -140,38 +136,6 @@ class EditDownloadClientModalContent extends Component {
/>
</FormGroup>
<FieldSet
size={sizes.SMALL}
legend={translate('CompletedDownloadHandling')}
>
<FormGroup>
<FormLabel>{translate('RemoveCompleted')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeCompletedDownloads"
helpText={translate('RemoveCompletedDownloadsHelpText')}
{...removeCompletedDownloads}
onChange={onInputChange}
/>
</FormGroup>
{
protocol.value !== 'torrent' &&
<FormGroup>
<FormLabel>{translate('RemoveFailed')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeFailedDownloads"
helpText={translate('RemoveFailedDownloadsHelpText')}
{...removeFailedDownloads}
onChange={onInputChange}
/>
</FormGroup>
}
</FieldSet>
</Form>
}
</ModalBody>

View File

@@ -1,13 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import { inputTypes, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function DownloadClientOptions(props) {
@@ -35,15 +34,11 @@ function DownloadClientOptions(props) {
}
{
hasSettings && !isFetching && !error && advancedSettings &&
hasSettings && !isFetching && !error &&
<div>
<FieldSet legend={translate('CompletedDownloadHandling')}>
<Form>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
@@ -55,6 +50,22 @@ function DownloadClientOptions(props) {
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Remove')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeCompletedDownloads"
helpText={translate('RemoveCompletedDownloadsHelpText')}
onChange={onInputChange}
{...settings.removeCompletedDownloads}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
@@ -91,10 +102,23 @@ function DownloadClientOptions(props) {
{...settings.autoRedownloadFailed}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Remove')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeFailedDownloads"
helpText={translate('RemoveFailedDownloadsHelpText')}
onChange={onInputChange}
{...settings.removeFailedDownloads}
/>
</FormGroup>
</Form>
<Alert kind={kinds.INFO}>
{translate('RemoveDownloadsAlert')}
</Alert>
</FieldSet>
</div>
}

View File

@@ -1,20 +1,23 @@
.remotePathMapping {
display: flex;
align-items: left;
align-items: stretch;
margin-bottom: 10px;
height: 30px;
border-bottom: 1px solid $borderColor;
line-height: 30px;
}
.actions {
flex: 0 0 25px;
}
.host {
flex: 1 0 300px;
flex: 0 0 300px;
}
.path {
flex: 1 0 400px;
flex: 0 0 400px;
}
.actions {
display: flex;
justify-content: flex-end;
flex: 1 0 auto;
padding-right: 10px;
}

View File

@@ -66,6 +66,9 @@ class RemotePathMapping extends Component {
styles.remotePathMapping
)}
>
<div className={styles.host}>{host}</div>
<div className={styles.path}>{remotePath}</div>
<div className={styles.path}>{localPath}</div>
<div className={styles.actions}>
<Link
@@ -75,10 +78,6 @@ class RemotePathMapping extends Component {
</Link>
</div>
<div className={styles.host}>{host}</div>
<div className={styles.path}>{remotePath}</div>
<div className={styles.path}>{localPath}</div>
<EditRemotePathMappingModalConnector
id={id}
isOpen={this.state.isEditRemotePathMappingModalOpen}

View File

@@ -4,19 +4,17 @@
font-weight: bold;
}
.actions {
flex: 0 0 25px;
}
.host {
flex: 1 0 300px;
flex: 0 0 300px;
}
.path {
flex: 1 0 400px;
flex: 0 0 400px;
}
.addRemotePathMapping {
display: flex;
justify-content: flex-end;
padding-right: 10px;
}

View File

@@ -51,15 +51,9 @@ class RemotePathMappings extends Component {
{...otherProps}
>
<div className={styles.remotePathMappingsHeader}>
<div className={styles.host}>
{translate('Host')}
</div>
<div className={styles.path}>
{translate('RemotePath')}
</div>
<div className={styles.path}>
{translate('LocalPath')}
</div>
<div className={styles.host}>Host</div>
<div className={styles.path}>Remote Path</div>
<div className={styles.path}>Local Path</div>
</div>
<div>

View File

@@ -2,16 +2,16 @@ import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector';
import EditImportExclusionModalContentConnector from './EditImportExclusionModalContentConnector';
function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) {
function EditImportExclusionModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditImportListExclusionModalContentConnector
<EditImportExclusionModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
@@ -19,9 +19,9 @@ function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) {
);
}
EditImportListExclusionModal.propTypes = {
EditImportExclusionModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditImportListExclusionModal;
export default EditImportExclusionModal;

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditImportListExclusionModal from './EditImportListExclusionModal';
import EditImportExclusionModal from './EditImportExclusionModal';
function mapStateToProps() {
return {};
@@ -12,7 +12,7 @@ const mapDispatchToProps = {
clearPendingChanges
};
class EditImportListExclusionModalConnector extends Component {
class EditImportExclusionModalConnector extends Component {
//
// Listeners
@@ -27,7 +27,7 @@ class EditImportListExclusionModalConnector extends Component {
render() {
return (
<EditImportListExclusionModal
<EditImportExclusionModal
{...this.props}
onModalClose={this.onModalClose}
/>
@@ -35,9 +35,9 @@ class EditImportListExclusionModalConnector extends Component {
}
}
EditImportListExclusionModalConnector.propTypes = {
EditImportExclusionModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector);
export default connect(mapStateToProps, mapDispatchToProps)(EditImportExclusionModalConnector);

View File

@@ -13,9 +13,9 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './EditImportListExclusionModalContent.css';
import styles from './EditImportExclusionModalContent.css';
function EditImportListExclusionModalContent(props) {
function EditImportExclusionModalContent(props) {
const {
id,
isFetching,
@@ -130,7 +130,7 @@ function EditImportListExclusionModalContent(props) {
);
}
EditImportListExclusionModalContent.propTypes = {
EditImportExclusionModalContent.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
@@ -143,4 +143,4 @@ EditImportListExclusionModalContent.propTypes = {
onDeleteImportExclusionPress: PropTypes.func
};
export default EditImportListExclusionModalContent;
export default EditImportExclusionModalContent;

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveImportExclusion, setImportExclusionValue } from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings';
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
import EditImportExclusionModalContent from './EditImportExclusionModalContent';
const newImportExclusion = {
movieTitle: '',
@@ -97,7 +97,7 @@ class EditImportExclusionModalContentConnector extends Component {
render() {
return (
<EditImportListExclusionModalContent
<EditImportExclusionModalContent
{...this.props}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}

View File

@@ -8,14 +8,12 @@
}
.movieTitle {
@add-mixin truncate;
flex: 0 0 600px;
flex: 0 0 400px;
}
.tmdbId,
.movieYear {
flex: 0 0 70px;
flex: 0 0 200px;
}
.actions {

View File

@@ -6,10 +6,10 @@ import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
import styles from './ImportListExclusion.css';
import EditImportExclusionModalConnector from './EditImportExclusionModalConnector';
import styles from './ImportExclusion.css';
class ImportListExclusion extends Component {
class ImportExclusion extends Component {
//
// Lifecycle
@@ -78,7 +78,7 @@ class ImportListExclusion extends Component {
</Link>
</div>
<EditImportListExclusionModalConnector
<EditImportExclusionModalConnector
id={id}
isOpen={this.state.isEditImportExclusionModalOpen}
onModalClose={this.onEditImportExclusionModalClose}
@@ -99,7 +99,7 @@ class ImportListExclusion extends Component {
}
}
ImportListExclusion.propTypes = {
ImportExclusion.propTypes = {
id: PropTypes.number.isRequired,
movieTitle: PropTypes.string.isRequired,
tmdbId: PropTypes.number.isRequired,
@@ -107,9 +107,9 @@ ImportListExclusion.propTypes = {
onConfirmDeleteImportExclusion: PropTypes.func.isRequired
};
ImportListExclusion.defaultProps = {
ImportExclusion.defaultProps = {
// The drag preview will not connect the drag handle.
connectDragSource: (node) => node
};
export default ImportListExclusion;
export default ImportExclusion;

View File

@@ -1,16 +1,16 @@
.importListExclusionsHeader {
.importExclusionsHeader {
display: flex;
margin-bottom: 10px;
font-weight: bold;
}
.title {
flex: 0 0 600px;
flex: 0 0 400px;
}
.tmdbId,
.movieYear {
flex: 0 0 70px;
flex: 0 0 200px;
}
.addImportExclusion {

View File

@@ -6,11 +6,11 @@ import Link from 'Components/Link/Link';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
import ImportListExclusion from './ImportListExclusion';
import styles from './ImportListExclusions.css';
import EditImportExclusionModalConnector from './EditImportExclusionModalConnector';
import ImportExclusion from './ImportExclusion';
import styles from './ImportExclusions.css';
class ImportListExclusions extends Component {
class ImportExclusions extends Component {
//
// Lifecycle
@@ -51,22 +51,16 @@ class ImportListExclusions extends Component {
{...otherProps}
>
<div className={styles.importExclusionsHeader}>
<div className={styles.tmdbId}>
TMDb Id
</div>
<div className={styles.title}>
{translate('Title')}
</div>
<div className={styles.movieYear}>
{translate('Year')}
</div>
<div className={styles.tmdbId}>TMDB Id</div>
<div className={styles.title}>Title</div>
<div className={styles.movieYear}>Year</div>
</div>
<div>
{
items.map((item, index) => {
return (
<ImportListExclusion
<ImportExclusion
key={item.id}
{...item}
{...otherProps}
@@ -87,7 +81,7 @@ class ImportListExclusions extends Component {
</Link>
</div>
<EditImportListExclusionModalConnector
<EditImportExclusionModalConnector
isOpen={this.state.isAddImportExclusionModalOpen}
onModalClose={this.onModalClose}
/>
@@ -98,11 +92,11 @@ class ImportListExclusions extends Component {
}
}
ImportListExclusions.propTypes = {
ImportExclusions.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteImportExclusion: PropTypes.func.isRequired
};
export default ImportListExclusions;
export default ImportExclusions;

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteImportExclusion, fetchImportExclusions } from 'Store/Actions/settingsActions';
import ImportListExclusions from './ImportListExclusions';
import ImportExclusions from './ImportExclusions';
function createMapStateToProps() {
return createSelector(
@@ -42,7 +42,7 @@ class ImportExclusionsConnector extends Component {
render() {
return (
<ImportListExclusions
<ImportExclusions
{...this.state}
{...this.props}
onConfirmDeleteImportExclusion={this.onConfirmDeleteImportExclusion}

View File

@@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import ImportListExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
import ImportExclusionsConnector from './ImportExclusions/ImportExclusionsConnector';
import ImportListsConnector from './ImportLists/ImportListsConnector';
import ImportListOptionsConnector from './Options/ImportListOptionsConnector';
@@ -86,7 +86,7 @@ class ImportListSettings extends Component {
onChildStateChange={this.onChildStateChange}
/>
<ImportListExclusionsConnector />
<ImportExclusionsConnector />
</PageContentBody>
</PageContent>

View File

@@ -45,9 +45,7 @@ function EditIndexerModalContent(props) {
supportsSearch,
tags,
fields,
priority,
protocol,
downloadClientId
priority
} = item;
return (
@@ -156,25 +154,8 @@ function EditIndexerModalContent(props) {
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}

View File

@@ -63,7 +63,6 @@ class Notification extends Component {
onMovieFileDelete,
onMovieFileDeleteForUpgrade,
onHealthIssue,
onApplicationUpdate,
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
@@ -71,8 +70,7 @@ class Notification extends Component {
supportsOnMovieDelete,
supportsOnMovieFileDelete,
supportsOnMovieFileDeleteForUpgrade,
supportsOnHealthIssue,
supportsOnApplicationUpdate
supportsOnHealthIssue
} = this.props;
return (
@@ -125,14 +123,6 @@ class Notification extends Component {
null
}
{
supportsOnApplicationUpdate && onApplicationUpdate ?
<Label kind={kinds.SUCCESS}>
{translate('OnApplicationUpdate')}
</Label> :
null
}
{
supportsOnMovieDelete && onMovieDelete ?
<Label kind={kinds.SUCCESS}>
@@ -158,7 +148,7 @@ class Notification extends Component {
}
{
!onGrab && !onDownload && !onRename && !onHealthIssue && !onApplicationUpdate && !onMovieDelete && !onMovieFileDelete ?
!onGrab && !onDownload && !onRename && !onHealthIssue && !onMovieDelete && !onMovieFileDelete ?
<Label
kind={kinds.DISABLED}
outline={true}
@@ -200,7 +190,6 @@ Notification.propTypes = {
onMovieFileDelete: PropTypes.bool.isRequired,
onMovieFileDeleteForUpgrade: PropTypes.bool.isRequired,
onHealthIssue: PropTypes.bool.isRequired,
onApplicationUpdate: PropTypes.bool.isRequired,
supportsOnGrab: PropTypes.bool.isRequired,
supportsOnDownload: PropTypes.bool.isRequired,
supportsOnMovieDelete: PropTypes.bool.isRequired,
@@ -209,7 +198,6 @@ Notification.propTypes = {
supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired,
supportsOnHealthIssue: PropTypes.bool.isRequired,
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired
};

View File

@@ -23,7 +23,6 @@ function NotificationEventItems(props) {
onMovieFileDelete,
onMovieFileDeleteForUpgrade,
onHealthIssue,
onApplicationUpdate,
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
@@ -31,7 +30,6 @@ function NotificationEventItems(props) {
supportsOnMovieDelete,
supportsOnMovieFileDelete,
supportsOnMovieFileDeleteForUpgrade,
supportsOnApplicationUpdate,
supportsOnHealthIssue,
includeHealthWarnings
} = item;
@@ -152,17 +150,6 @@ function NotificationEventItems(props) {
/>
</div>
}
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onApplicationUpdate"
helpText={translate('OnApplicationUpdateHelpText')}
isDisabled={!supportsOnApplicationUpdate.value}
{...onApplicationUpdate}
onChange={onInputChange}
/>
</div>
</div>
</div>
</FormGroup>

View File

@@ -82,18 +82,10 @@ class DelayProfiles extends Component {
>
<div>
<div className={styles.delayProfilesHeader}>
<div className={styles.column}>
{translate('Protocol')}
</div>
<div className={styles.column}>
{translate('UsenetDelay')}
</div>
<div className={styles.column}>
{translate('TorrentDelay')}
</div>
<div className={styles.tags}>
{translate('Tags')}
</div>
<div className={styles.column}>Protocol</div>
<div className={styles.column}>Usenet Delay</div>
<div className={styles.column}>Torrent Delay</div>
<div className={styles.tags}>Tags</div>
</div>
<div className={styles.delayProfiles}>

View File

@@ -25,15 +25,9 @@ class QualityDefinitions extends Component {
{...otherProps}
>
<div className={styles.header}>
<div className={styles.quality}>
{translate('Quality')}
</div>
<div className={styles.title}>
{translate('Title')}
</div>
<div className={styles.sizeLimit}>
{translate('SizeLimit')}
</div>
<div className={styles.quality}>Quality</div>
<div className={styles.title}>Title</div>
<div className={styles.sizeLimit}>Size Limit</div>
{
advancedSettings ?

View File

@@ -152,7 +152,7 @@ function TagDetailsModalContent(props) {
{
indexers.length ?
<FieldSet legend={translate('Indexers')}>
<FieldSet legend="Indexers">
{
indexers.map((item) => {
return (

View File

@@ -48,6 +48,21 @@ export const movieRuntimeFormatOptions = [
{ key: 'minutes', value: '75 mins' }
];
export const themeOptions = [
{ key: 'default', value: 'Default' },
{ key: 'aquamarine', value: 'Aquamarine' },
{ key: 'dark', value: 'Dark' },
{ key: 'dracula', value: 'Dracula' },
{ key: 'hotline', value: 'Hotline' },
{ key: 'hotpink', value: 'Hotpink' },
{ key: 'nord', value: 'Nord' },
{ key: 'organizr', value: 'Organizr' },
{ key: 'overseerr', value: 'Overseerr' },
{ key: 'plex', value: 'Plex' },
{ key: 'radarr-darker', value: 'Radarr Darker' },
{ key: 'space-gray', value: 'Space Gray' }
];
class UISettings extends Component {
//
@@ -184,6 +199,21 @@ class UISettings extends Component {
</FieldSet>
<FieldSet legend={translate('Style')}>
<FormGroup>
<FormLabel>{translate('Theme')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="theme"
helpText={translate('ThemeHelpText', ['Theme.Park'])}
inlineLink="https://github.com/GilbN/theme.park"
tooltip="Theme.Park Github"
helpTextWarning={translate('ThemeHelpTextWarning')}
values={themeOptions}
onChange={onInputChange}
{...settings.theme}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SettingsEnableColorImpairedMode')}</FormLabel>
<FormInputGroup

View File

@@ -109,7 +109,6 @@ export default {
selectedSchema.onMovieDelete = selectedSchema.supportsOnMovieDelete;
selectedSchema.onMovieFileDelete = selectedSchema.supportsOnMovieFileDelete;
selectedSchema.onMovieFileDeleteForUpgrade = selectedSchema.supportsOnMovieFileDeleteForUpgrade;
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;
return selectedSchema;
});

View File

@@ -600,7 +600,6 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({
url: '/exclusions/bulk',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(exclusions)
}).request;

View File

@@ -148,10 +148,9 @@ export const actionHandlers = handleThunks({
return {
id,
path: item.path,
movieId: item.movie ? item.movie.id : undefined,
movieId: item.movie.id,
quality: item.quality,
languages: item.languages,
releaseGroup: item.releaseGroup,
downloadId: item.downloadId
};
});

View File

@@ -27,7 +27,7 @@ const paged = `${section}.paged`;
export const defaultState = {
options: {
includeUnknownMovieItems: true
includeUnknownMovieItems: false
},
status: {

View File

@@ -1,6 +1,7 @@
import { createBrowserHistory } from 'history';
import React from 'react';
import { render } from 'react-dom';
import ThemeSelector from 'App/ThemeSelector';
import createAppStore from 'Store/createAppStore';
import App from './App/App';
@@ -13,9 +14,11 @@ const history = createBrowserHistory();
const store = createAppStore(history);
render(
<App
store={store}
history={history}
/>,
<ThemeSelector>
<App
store={store}
history={history}
/>
</ThemeSelector>,
document.getElementById('root')
);

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "6.0.0",
"@microsoft/signalr": "5.0.10",
"@sentry/browser": "6.13.2",
"@sentry/integrations": "6.13.2",
"classnames": "2.3.1",
@@ -82,7 +82,6 @@
},
"devDependencies": {
"@babel/core": "7.13.16",
"@babel/eslint-parser": "7.13.14",
"@babel/plugin-proposal-class-properties": "7.13.0",
"@babel/plugin-proposal-decorators": "7.13.15",
"@babel/plugin-proposal-export-default-from": "7.12.13",
@@ -95,6 +94,7 @@
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.13.15",
"@babel/preset-react": "7.13.13",
"@babel/eslint-parser": "7.13.14",
"autoprefixer": "10.2.5",
"babel-loader": "8.2.2",
"babel-plugin-inline-classnames": "2.0.1",
@@ -125,12 +125,12 @@
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "2.0.0",
"stylelint": "13.13.0",
"stylelint-order": "4.1.0",
"url-loader": "4.1.1",
"webpack": "5.35.1",
"webpack-cli": "4.6.0",
"webpack-livereload-plugin": "3.0.1",
"worker-loader": "3.0.8"
"worker-loader": "3.0.8",
"stylelint": "13.13.0",
"stylelint-order": "4.1.0"
}
}

View File

@@ -2,11 +2,10 @@
<!-- Common to all Radarr Projects -->
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<PlatformTarget>AnyCPU</PlatformTarget>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;osx-arm64;linux-x64;linux-musl-x64;linux-arm;linux-musl-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;linux-x64;linux-musl-x64;linux-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
<RadarrRootDir>$(MSBuildThisFileDirectory)..\</RadarrRootDir>
@@ -90,13 +89,13 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="NunitXml.TestLogger" Version="2.1.62" />
</ItemGroup>
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net5.0'">
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
</ItemGroup>

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net5.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBuilder" Version="6.1.0" />

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net5.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Selenium.Support" Version="3.141.0" />

View File

@@ -4,7 +4,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using FluentAssertions;
using Moq;
@@ -16,11 +15,8 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories;
using HttpClient = NzbDrone.Common.Http.HttpClient;
namespace NzbDrone.Common.Test.Http
{
@@ -35,8 +31,6 @@ namespace NzbDrone.Common.Test.Http
private string _httpBinHost;
private string _httpBinHost2;
private System.Net.Http.HttpClient _httpClient = new ();
[OneTimeSetUp]
public void FixtureSetUp()
{
@@ -44,7 +38,7 @@ namespace NzbDrone.Common.Test.Http
var mainHost = "httpbin.servarr.com";
// Use mirrors for tests that use two hosts
var candidates = new[] { "httpbin1.servarr.com" };
var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
_httpBinHost = mainHost;
@@ -52,20 +46,29 @@ namespace NzbDrone.Common.Test.Http
TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = 10;
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10;
}
private bool IsTestSiteAvailable(string site)
{
try
{
var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest;
var res = req.GetResponse() as HttpWebResponse;
if (res.StatusCode != HttpStatusCode.OK)
{
return false;
}
res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
try
{
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
if (res == null || res.StatusCode != (HttpStatusCode)429)
{
@@ -92,13 +95,10 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
@@ -138,28 +138,6 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace();
}
[TestCase(CertificateValidationType.Enabled)]
[TestCase(CertificateValidationType.DisabledForLocalAddresses)]
public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType)
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void bad_ssl_should_pass_if_remote_validation_disabled()
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request);
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_execute_typed_get()
{
@@ -184,44 +162,15 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_post_with_content_type()
[TestCase("gzip")]
public void should_execute_get_using_gzip(string compression)
{
var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message);
request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_get_using_gzip()
{
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var request = new HttpRequest($"https://{_httpBinHost}/{compression}");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression);
response.Resource.Gzipped.Should().BeTrue();
response.Resource.Brotli.Should().BeFalse();
}
[Test]
public void should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
response.Resource.Gzipped.Should().BeFalse();
response.Resource.Brotli.Should().BeTrue();
}
[TestCase(HttpStatusCode.Unauthorized)]
@@ -388,38 +337,13 @@ namespace NzbDrone.Common.Test.Http
{
var file = GetTempFilePath();
Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
Assert.Throws<WebException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_write_redirect_content_to_stream()
{
var file = GetTempFilePath();
using (var fileStream = new FileStream(file, FileMode.Create))
{
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false;
request.ResponseStream = fileStream;
var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved);
}
ExceptionVerification.ExpectedErrors(1);
File.Exists(file).Should().BeTrue();
var fileInfo = new FileInfo(file);
fileInfo.Length.Should().Be(0);
}
[Test]
public void should_send_cookie()
{
@@ -819,7 +743,7 @@ namespace NzbDrone.Common.Test.Http
{
try
{
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}";
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;
@@ -849,7 +773,6 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; }
public string Data { get; set; }
public bool Gzipped { get; set; }
public bool Brotli { get; set; }
}
public class HttpCookieResource

View File

@@ -62,7 +62,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase("Hardlinking episode file: /home/mySecret/Downloads to /media/abc.mkv")]
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
// Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net5.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Host\Radarr.Host.csproj" />

View File

@@ -3,8 +3,6 @@ using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
@@ -27,15 +25,12 @@ namespace NzbDrone.Common.Test
.AddNzbDroneLogger()
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second"));
.AddStartupContext(new StartupContext("first", "second"))
.GetServiceProvider();
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
container.GetRequiredService<IAppFolderFactory>().Register();
var serviceProvider = container.GetServiceProvider();
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
Mocker.SetConstant<System.IServiceProvider>(serviceProvider);
Mocker.SetConstant<System.IServiceProvider>(container);
var handlers = Subject.BuildAll<IHandle<ApplicationStartedEvent>>()
.Select(c => c.GetType().FullName);

View File

@@ -106,7 +106,7 @@ namespace NzbDrone.Common
Stream inStream = File.OpenRead(compressedFile);
Stream gzipStream = new GZipInputStream(inStream);
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null);
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream);
tarArchive.ExtractContents(destination);
tarArchive.Close();

View File

@@ -1,9 +1,9 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Principal;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using System.ServiceProcess;
using NLog;
using NzbDrone.Common.Processes;
@@ -14,13 +14,16 @@ namespace NzbDrone.Common.EnvironmentInfo
private readonly Logger _logger;
private readonly DateTime _startTime = DateTime.UtcNow;
public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null)
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
{
_logger = logger;
IsWindowsService = hostLifetime is WindowsServiceLifetime;
IsWindowsService = !IsUserInteractive &&
OsInfo.IsWindows &&
serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) &&
serviceProvider.GetStatus(ServiceProvider.SERVICE_NAME) == ServiceControllerStatus.StartPending;
// net6.0 will return Radarr.dll for entry assembly, we need the actual
// net5.0 will return Radarr.dll for entry assembly, we need the actual
// executable name (Radarr on linux). On mono this will return the location of
// the mono executable itself, which is not what we want.
var entry = Process.GetCurrentProcess().MainModule;

View File

@@ -6,6 +6,13 @@ namespace NzbDrone.Common.Extensions
{
public static class EnumerableExtensions
{
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
var knownKeys = new HashSet<TKey>();
return source.Where(element => knownKeys.Add(keySelector(element)));
}
public static IEnumerable<TFirst> IntersectBy<TFirst, TSecond, TKey>(this IEnumerable<TFirst> first,
Func<TFirst, TKey> firstKeySelector,
IEnumerable<TSecond> second,

View File

@@ -1,11 +0,0 @@
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace NzbDrone.Common.Http.Dispatchers
{
public interface ICertificateValidationService
{
bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors);
}
}

View File

@@ -5,5 +5,6 @@ namespace NzbDrone.Common.Http.Dispatchers
public interface IHttpDispatcher
{
HttpResponse GetResponse(HttpRequest request, CookieContainer cookies);
void DownloadFile(string url, string fileName);
}
}

View File

@@ -1,13 +1,12 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection;
using NLog;
using NzbDrone.Common.Cache;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
@@ -15,171 +14,200 @@ namespace NzbDrone.Common.Http.Dispatchers
{
public class ManagedHttpDispatcher : IHttpDispatcher
{
private const string NO_PROXY_KEY = "no-proxy";
private const int connection_establish_timeout = 2000;
private static bool useIPv6 = Socket.OSSupportsIPv6;
private static bool hasResolvedIPv6Availability;
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly ICertificateValidationService _certificateValidationService;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy;
_certificateValidationService = certificateValidationService;
_userAgentBuilder = userAgentBuilder;
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
_platformInfo = platformInfo;
_logger = logger;
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
if (cookieHeader.IsNotNullOrWhiteSpace())
{
requestMessage.Headers.Add("Cookie", cookieHeader);
}
// Deflate is not a standard and could break depending on implementation.
// we should just stick with the more compatible Gzip
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
using var cts = new CancellationTokenSource();
if (request.RequestTimeout != TimeSpan.Zero)
{
cts.CancelAfter(request.RequestTimeout);
}
else
{
// The default for System.Net.Http.HttpClient
cts.CancelAfter(TimeSpan.FromSeconds(100));
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
}
if (request.ContentData != null)
{
requestMessage.Content = new ByteArrayContent(request.ContentData);
}
webRequest.Proxy = GetProxy(request.Url);
if (request.Headers != null)
{
AddRequestHeaders(requestMessage, request.Headers);
AddRequestHeaders(webRequest, request.Headers);
}
var httpClient = GetClient(request.Url);
HttpWebResponse httpWebResponse;
using var responseMessage = httpClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
try
{
byte[] data = null;
try
if (request.ContentData != null)
{
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
webRequest.ContentLength = request.ContentData.Length;
using (var writeStream = webRequest.GetRequestStream())
{
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
}
}
httpWebResponse = (HttpWebResponse)webRequest.GetResponse();
}
catch (WebException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{
// The default messages for WebException on mono are pretty horrible.
if (e.Status == WebExceptionStatus.NameResolutionFailure)
{
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status);
}
else if (e.ToString().Contains("TLS Support not"))
{
throw new TlsFailureException(webRequest, e);
}
else if (e.ToString().Contains("The authentication or decryption has failed."))
{
throw new TlsFailureException(webRequest, e);
}
else if (OsInfo.IsNotWindows)
{
throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
}
else
{
data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
throw;
}
}
catch (Exception ex)
}
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null && responseStream != Stream.Null)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
try
{
data = responseStream.ToBytes();
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
}
}
}
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
}
public void DownloadFile(string url, string fileName)
{
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
var headers = responseMessage.Headers.ToNameValueCollection();
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
var stopWatch = Stopwatch.StartNew();
var uri = new HttpUri(url);
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode);
using (var webClient = new GZipWebClient())
{
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.Proxy = GetProxy(uri);
webClient.DownloadFile(uri.FullUri, fileName);
stopWatch.Stop();
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
}
catch (WebException e)
{
_logger.Warn("Failed to get response from: {0} {1}", url, e.Message);
throw;
}
catch (Exception e)
{
_logger.Warn(e, "Failed to get response from: " + url);
throw;
}
}
protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri)
protected virtual IWebProxy GetProxy(HttpUri uri)
{
IWebProxy proxy = null;
var proxySettings = _proxySettingsProvider.GetProxySettings(uri);
var key = proxySettings?.Key ?? NO_PROXY_KEY;
return _httpClientCache.Get(key, () => CreateHttpClient(proxySettings));
}
protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings)
{
var handler = new SocketsHttpHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
UseCookies = false, // sic - we don't want to use a shared cookie container
AllowAutoRedirect = false,
MaxConnectionsPerServer = 12,
ConnectCallback = onConnect,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
}
};
if (proxySettings != null)
{
handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
}
var client = new System.Net.Http.HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
return client;
return proxy;
}
protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers)
{
foreach (var header in headers)
{
switch (header.Key)
{
case "Accept":
webRequest.Headers.Accept.ParseAdd(header.Value);
webRequest.Accept = header.Value;
break;
case "Connection":
webRequest.Headers.Connection.Clear();
webRequest.Headers.Connection.Add(header.Value);
webRequest.Connection = header.Value;
break;
case "Content-Length":
AddContentHeader(webRequest, "Content-Length", header.Value);
webRequest.ContentLength = Convert.ToInt64(header.Value);
break;
case "Content-Type":
AddContentHeader(webRequest, "Content-Type", header.Value);
webRequest.ContentType = header.Value;
break;
case "Date":
webRequest.Headers.Remove("Date");
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
webRequest.Date = HttpHeader.ParseDateTime(header.Value);
break;
case "Expect":
webRequest.Headers.Expect.ParseAdd(header.Value);
webRequest.Expect = header.Value;
break;
case "Host":
webRequest.Headers.Host = header.Value;
webRequest.Host = header.Value;
break;
case "If-Modified-Since":
webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
break;
case "Range":
throw new NotImplementedException();
case "Referer":
webRequest.Headers.Add("Referer", header.Value);
webRequest.Referer = header.Value;
break;
case "Transfer-Encoding":
webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
webRequest.TransferEncoding = header.Value;
break;
case "User-Agent":
throw new NotSupportedException("User-Agent other than Radarr not allowed.");
@@ -191,79 +219,5 @@ namespace NzbDrone.Common.Http.Dispatchers
}
}
}
private void AddContentHeader(HttpRequestMessage request, string header, string value)
{
var headers = request.Content?.Headers;
if (headers == null)
{
return;
}
headers.Remove(header);
headers.Add(header, value);
}
private static 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.
if (useIPv6)
{
try
{
var localToken = cancellationToken;
if (!hasResolvedIPv6Availability)
{
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
var quickFailCts = new CancellationTokenSource(connection_establish_timeout);
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
localToken = linkedTokenSource.Token;
}
return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken);
}
catch
{
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
// but in the interest of keeping this implementation simple, this is acceptable.
useIPv6 = false;
}
finally
{
hasResolvedIPv6Availability = true;
}
}
// fallback to IPv4.
return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
}
private static async ValueTask<Stream> attemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
{
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
NoDelay = true
};
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
// The stream should take the ownership of the underlying socket,
// closing it when it's disposed.
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Net;
namespace NzbDrone.Common.Http
{
public class GZipWebClient : WebClient
{
protected override WebRequest GetWebRequest(Uri address)
{
var request = (HttpWebRequest)base.GetWebRequest(address);
request.AutomaticDecompression = DecompressionMethods.GZip;
return request;
}
}
}

View File

@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
@@ -121,6 +119,8 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew();
PrepareRequestCookies(request, cookieContainer);
var response = _httpDispatcher.GetResponse(request, cookieContainer);
HandleResponseCookies(response, cookieContainer);
@@ -187,98 +187,57 @@ namespace NzbDrone.Common.Http
}
}
private void HandleResponseCookies(HttpResponse response, CookieContainer container)
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer)
{
foreach (Cookie cookie in container.GetAllCookies())
// Don't collect persistnet cookies for intermediate/redirected urls.
/*lock (_cookieContainerCache)
{
cookie.Expired = true;
}
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
cookieContainer.Add(persistentCookies);
cookieContainer.Add(existingCookies);
}*/
}
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer)
{
var cookieHeaders = response.GetCookieHeaders();
if (cookieHeaders.Empty())
{
return;
}
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
if (response.Request.StoreResponseCookie)
{
lock (_cookieContainerCache)
{
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
}
}
}
private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
{
foreach (var cookieHeader in cookieHeaders)
{
try
{
container.SetCookies((Uri)url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", url);
foreach (var cookieHeader in cookieHeaders)
{
try
{
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
}
}
}
}
}
public void DownloadFile(string url, string fileName)
{
var fileNamePart = fileName + ".part";
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
{
var request = new HttpRequest(url);
request.AllowAutoRedirect = true;
request.ResponseStream = fileStream;
var response = Get(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new HttpException(request, response, "Site responded with html content.");
}
}
stopWatch.Stop();
if (File.Exists(fileName))
{
File.Delete(fileName);
}
File.Move(fileNamePart, fileName);
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
finally
{
if (File.Exists(fileNamePart))
{
File.Delete(fileNamePart);
}
}
_httpDispatcher.DownloadFile(url, fileName);
}
public HttpResponse Get(HttpRequest request)
{
request.Method = HttpMethod.Get;
request.Method = HttpMethod.GET;
return Execute(request);
}
@@ -292,13 +251,13 @@ namespace NzbDrone.Common.Http
public HttpResponse Head(HttpRequest request)
{
request.Method = HttpMethod.Head;
request.Method = HttpMethod.HEAD;
return Execute(request);
}
public HttpResponse Post(HttpRequest request)
{
request.Method = HttpMethod.Post;
request.Method = HttpMethod.POST;
return Execute(request);
}

View File

@@ -1,30 +1,14 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http
{
public static class WebHeaderCollectionExtensions
{
public static NameValueCollection ToNameValueCollection(this HttpHeaders headers)
{
var result = new NameValueCollection();
foreach (var header in headers)
{
result.Add(header.Key, header.Value.ConcatToString(";"));
}
return result;
}
}
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
{
public HttpHeader(NameValueCollection headers)
@@ -32,11 +16,6 @@ namespace NzbDrone.Common.Http
{
}
public HttpHeader(HttpHeaders headers)
: base(headers.ToNameValueCollection())
{
}
public HttpHeader()
{
}

View File

@@ -0,0 +1,14 @@
namespace NzbDrone.Common.Http
{
public enum HttpMethod
{
GET,
POST,
PUT,
DELETE,
HEAD,
OPTIONS,
PATCH,
MERGE
}
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -13,7 +11,6 @@ namespace NzbDrone.Common.Http
{
public HttpRequest(string url, HttpAccept httpAccept = null)
{
Method = HttpMethod.Get;
Url = new HttpUri(url);
Headers = new HttpHeader();
AllowAutoRedirect = true;
@@ -52,7 +49,6 @@ namespace NzbDrone.Common.Http
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; }
public string RateLimitKey { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString()
{

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using NzbDrone.Common.Extensions;
@@ -36,7 +35,7 @@ namespace NzbDrone.Common.Http
{
BaseUrl = new HttpUri(baseUrl);
ResourceUrl = string.Empty;
Method = HttpMethod.Get;
Method = HttpMethod.GET;
QueryParams = new List<KeyValuePair<string, string>>();
SuffixQueryParams = new List<KeyValuePair<string, string>>();
Segments = new Dictionary<string, string>();
@@ -272,7 +271,7 @@ namespace NzbDrone.Common.Http
public virtual HttpRequestBuilder Post()
{
Method = HttpMethod.Post;
Method = HttpMethod.POST;
return this;
}
@@ -363,7 +362,7 @@ namespace NzbDrone.Common.Http
public virtual HttpRequestBuilder AddFormParameter(string key, object value)
{
if (Method != HttpMethod.Post)
if (Method != HttpMethod.POST)
{
throw new NotSupportedException("HttpRequest Method must be POST to add FormParameter.");
}
@@ -379,7 +378,7 @@ namespace NzbDrone.Common.Http
public virtual HttpRequestBuilder AddFormUpload(string name, string fileName, byte[] data, string contentType = "application/octet-stream")
{
if (Method != HttpMethod.Post)
if (Method != HttpMethod.POST)
{
throw new NotSupportedException("HttpRequest Method must be POST to add FormUpload.");
}

View File

@@ -1,7 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Newtonsoft.Json;
using NzbDrone.Common.Serializer;
@@ -18,14 +17,14 @@ namespace NzbDrone.Common.Http
public JsonRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.Post;
Method = HttpMethod.POST;
JsonParameters = new List<object>();
}
public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable<object> parameters)
: base(baseUrl)
{
Method = HttpMethod.Post;
Method = HttpMethod.POST;
JsonMethod = method;
JsonParameters = parameters.ToList();
}

View File

@@ -1,5 +1,9 @@
using System;
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using com.LandonKey.SocksWebProxy;
using com.LandonKey.SocksWebProxy.Proxy;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
@@ -29,37 +33,54 @@ namespace NzbDrone.Common.Http.Proxy
}
private IWebProxy CreateWebProxy(HttpProxySettings proxySettings)
{
var uri = GetProxyUri(proxySettings);
if (uri == null)
{
return null;
}
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
{
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
}
else
{
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
}
}
private Uri GetProxyUri(HttpProxySettings proxySettings)
{
switch (proxySettings.Type)
{
case ProxyType.Http:
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
{
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
}
else
{
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
}
case ProxyType.Socks4:
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Four, proxySettings.Username, proxySettings.Password), false);
case ProxyType.Socks5:
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
default:
return null;
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Five, proxySettings.Username, proxySettings.Password), false);
}
return null;
}
private static IPAddress GetProxyIpAddress(string host)
{
IPAddress ipAddress;
if (!IPAddress.TryParse(host, out ipAddress))
{
try
{
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
}
catch (Exception e)
{
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
}
}
return ipAddress;
}
private static int GetNextFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
}

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&|: )(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -1,24 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net5.0</TargetFrameworks>
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.12" />
<PackageReference Include="DryIoc.dll" Version="4.7.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.11" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Sentry" Version="3.11.1" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.0" />
<PackageReference Include="Sentry" Version="3.9.3" />
<PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="5.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net5.0</TargetFrameworks>
<ApplicationIcon>..\NzbDrone.Host\Radarr.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="!$(RuntimeIdentifier.StartsWith('win'))">
<AssemblyName>Radarr</AssemblyName>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.net>
<connectionManagement>
<add address="*" maxconnection="100" />
</connectionManagement>
</system.net>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
</startup>
<runtime>
<loadFromRemoteSources enabled="true" />
</runtime>
</configuration>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>

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