Compare commits

..

1 Commits

Author SHA1 Message Date
Robin Dadswell
0be2aee9c3 Fixed: Instance Name validation 2022-05-16 21:29:00 +01:00
291 changed files with 8911 additions and 63037 deletions

View File

@@ -27,7 +27,10 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
## Support
Note: Prowlarr is currently early in life, thus bugs should be expected
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.4'
majorVersion: '0.4.0'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.301'
dotnetVersion: '6.0.201'
innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022'
@@ -97,14 +97,15 @@ stages:
- bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
echo "BSD already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi
displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-extra-platforms
displayName: Enable FreeBSD Support
- bash: ./build.sh --backend --enable-bsd
displayName: Build Prowlarr Backend
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -118,28 +119,24 @@ stages:
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
artifact: WindowsCoreTests
displayName: Publish Windows Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
artifact: LinuxCoreTests
displayName: Publish Linux Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
artifact: MacCoreTests
displayName: Publish MacOS Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend
@@ -242,35 +239,35 @@ stages:
artifactName: WindowsFrontend
targetPath: _output
displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-extra-platforms
- bash: ./build.sh --packages --enable-bsd
displayName: Create Packages
- bash: |
find . -name "Prowlarr" -exec chmod a+x {} \;
find . -name "Prowlarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits
- task: ArchiveFiles@2
displayName: Create win-x64 zip
displayName: Create Windows Core zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2
displayName: Create win-x86 zip
displayName: Create Windows x86 Core zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2
displayName: Create osx-x64 app
displayName: Create MacOS x64 Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2
displayName: Create osx-x64 tar
displayName: Create MacOS x64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar'
@@ -278,14 +275,14 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 app
displayName: Create MacOS arm64 Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 tar
displayName: Create MacOS arm64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar'
@@ -293,7 +290,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x64 tar
displayName: Create Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar'
@@ -301,7 +298,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-musl-x64 tar
displayName: Create Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar'
@@ -309,15 +306,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
displayName: Create ARM32 Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar'
@@ -325,7 +314,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm tar
displayName: Create ARM32 Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar'
@@ -333,7 +322,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm64 tar
displayName: Create ARM64 Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar'
@@ -341,7 +330,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm64 tar
displayName: Create ARM64 Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar'
@@ -416,22 +405,22 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'osx-x64'
testName: 'MacCore'
poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }}
WindowsCore:
osName: 'Windows'
testName: 'win-x64'
testName: 'WindowsCore'
poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }}
LinuxCore:
osName: 'Linux'
testName: 'linux-x64'
testName: 'LinuxCore'
poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }}
FreebsdCore:
osName: 'Linux'
testName: 'freebsd-x64'
testName: 'FreebsdCore'
poolName: 'FreeBSD'
imageName:
@@ -450,7 +439,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)-tests'
artifactName: '$(testName)Tests'
targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
@@ -480,12 +469,8 @@ stages:
matrix:
alpine:
testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests
artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool:
vmImage: ${{ variables.linuxImage }}
@@ -496,15 +481,9 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -527,58 +506,6 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: 'ubuntu-18.04'
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
displayName: Integration
@@ -606,17 +533,17 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'osx-x64'
testName: 'MacCore'
imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
WindowsCore:
osName: 'Windows'
testName: 'win-x64'
testName: 'WindowsCore'
imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip'
LinuxCore:
osName: 'Linux'
testName: 'linux-x64'
testName: 'LinuxCore'
imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
@@ -633,7 +560,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)-tests'
artifactName: '$(testName)Tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -663,67 +590,6 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: 'ubuntu-18.04'
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD
displayName: Integration Native FreeBSD
dependsOn: Prepare
@@ -741,7 +607,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'freebsd-x64-tests'
artifactName: 'FreebsdCoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -777,15 +643,11 @@ stages:
strategy:
matrix:
alpine:
testName: 'linux-musl-x64'
artifactName: linux-musl-x64-tests
testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Prowlarr.*.linux-core-x86.tar.gz'
pool:
vmImage: ${{ variables.linuxImage }}
@@ -795,15 +657,9 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -849,19 +705,16 @@ stages:
matrix:
Linux:
osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
failBuild: true
Mac:
osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows:
osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip'
failBuild: true
@@ -879,7 +732,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(artifactName)-tests'
artifactName: '$(osName)CoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact

View File

@@ -25,22 +25,15 @@ UpdateVersionNumber()
fi
}
EnableExtraPlatformsInSDK()
EnableBsdSupport()
{
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
#todo enable sdk with
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
fi
}
@@ -300,8 +293,7 @@ if [ $# -eq 0 ]; then
PACKAGES=YES
INSTALLER=NO
LINT=YES
ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
ENABLE_BSD=NO
fi
while [[ $# -gt 0 ]]
@@ -313,12 +305,8 @@ case $key in
BACKEND=YES
shift # past argument
;;
--enable-bsd|--enable-extra-platforms)
ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
--enable-bsd)
ENABLE_BSD=YES
shift # past argument
;;
-r|--runtime)
@@ -362,17 +350,12 @@ esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ];
then
UpdateVersionNumber
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
if [ "$ENABLE_BSD" = "YES" ];
then
EnableExtraPlatforms
EnableBsdSupport
fi
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -382,10 +365,9 @@ then
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
if [ "$ENABLE_BSD" = "YES" ];
then
PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi
else
PackageTests "$FRAMEWORK" "$RID"
@@ -424,10 +406,9 @@ then
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
if [ "$ENABLE_BSD" = "YES" ];
then
Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi
else
Package "$FRAMEWORK" "$RID"

View File

@@ -78,7 +78,7 @@
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
background-color: var(--white);
}
.loading {

View File

@@ -74,7 +74,7 @@ class PageHeader extends Component {
<IconButton
className={styles.donate}
name={icons.HEART}
to="https://prowlarr.com/donate"
to="https://opencollective.com/prowlarr"
size={14}
/>
<IconButton

View File

@@ -7,7 +7,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
(state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated,
(state) => state.settings.general.isPopulated,
(state) => state.settings.languages.isPopulated,
(state) => state.settings.appProfiles.isPopulated,
(state) => state.indexers.isPopulated,
(state) => state.indexerStatus.isPopulated,
@@ -58,6 +59,7 @@ const selectIsPopulated = createSelector(
tagsIsPopulated,
uiSettingsIsPopulated,
generalSettingsIsPopulated,
languagesIsPopulated,
appProfilesIsPopulated,
indexersIsPopulated,
indexerStatusIsPopulated,
@@ -69,6 +71,7 @@ const selectIsPopulated = createSelector(
tagsIsPopulated &&
uiSettingsIsPopulated &&
generalSettingsIsPopulated &&
languagesIsPopulated &&
appProfilesIsPopulated &&
indexersIsPopulated &&
indexerStatusIsPopulated &&
@@ -83,6 +86,7 @@ const selectErrors = createSelector(
(state) => state.tags.error,
(state) => state.settings.ui.error,
(state) => state.settings.general.error,
(state) => state.settings.languages.error,
(state) => state.settings.appProfiles.error,
(state) => state.indexers.error,
(state) => state.indexerStatus.error,
@@ -93,6 +97,7 @@ const selectErrors = createSelector(
tagsError,
uiSettingsError,
generalSettingsError,
languagesError,
appProfilesError,
indexersError,
indexerStatusError,
@@ -104,6 +109,7 @@ const selectErrors = createSelector(
tagsError ||
uiSettingsError ||
generalSettingsError ||
languagesError ||
appProfilesError ||
indexersError ||
indexerStatusError ||
@@ -117,6 +123,7 @@ const selectErrors = createSelector(
tagsError,
uiSettingsError,
generalSettingsError,
languagesError,
appProfilesError,
indexersError,
indexerStatusError,
@@ -159,6 +166,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchTags() {
dispatch(fetchTags());
},
dispatchFetchLanguages() {
dispatch(fetchLanguages());
},
dispatchFetchIndexers() {
dispatch(fetchIndexers());
},
@@ -206,6 +216,7 @@ class PageConnector extends Component {
if (!this.props.isPopulated) {
this.props.dispatchFetchCustomFilters();
this.props.dispatchFetchTags();
this.props.dispatchFetchLanguages();
this.props.dispatchFetchAppProfiles();
this.props.dispatchFetchIndexers();
this.props.dispatchFetchIndexerStatus();
@@ -231,6 +242,7 @@ class PageConnector extends Component {
isPopulated,
hasError,
dispatchFetchTags,
dispatchFetchLanguages,
dispatchFetchAppProfiles,
dispatchFetchIndexers,
dispatchFetchIndexerStatus,
@@ -271,6 +283,7 @@ PageConnector.propTypes = {
isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchCustomFilters: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchAppProfiles: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchFetchIndexerStatus: PropTypes.func.isRequired,

View File

@@ -20,9 +20,9 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.MOVIE_CONTINUING,
title: translate('Indexers'),
title: 'Indexers',
to: '/',
alias: '/indexers',
alias: '/movies',
children: [
{
title: translate('Stats'),
@@ -33,13 +33,13 @@ const links = [
{
iconName: icons.SEARCH,
title: translate('Search'),
title: 'Search',
to: '/search'
},
{
iconName: icons.ACTIVITY,
title: translate('History'),
title: 'History',
to: '/history'
},

View File

@@ -7,7 +7,7 @@
position: relative;
&.default {
background-color: var(--popoverBodyBackgroundColor);
background-color: var(--white);
box-shadow: 0 5px 10px var(--popoverShadowColor);
}

View File

@@ -35,21 +35,21 @@ class IndexerIndexFooter extends PureComponent {
<div className={styles.legendItem}>
<div className={styles.enabled} />
<div>
{translate('Enabled')}
Enabled
</div>
</div>
<div className={styles.legendItem}>
<div className={styles.redirected} />
<div>
{translate('EnabledRedirected')}
Enabled, Redirected
</div>
</div>
<div className={styles.legendItem}>
<div className={styles.disabled} />
<div>
{translate('Disabled')}
Disabled
</div>
</div>
@@ -60,7 +60,7 @@ class IndexerIndexFooter extends PureComponent {
)}
/>
<div>
{translate('Error')}
Error
</div>
</div>
</div>

View File

@@ -24,7 +24,6 @@ const searchOptions = [
const seriesTokens = [
{ token: '{ImdbId:tt1234567}', example: 'tt12345' },
{ token: '{TvdbId:12345}', example: '12345' },
{ token: '{TmdbId:12345}', example: '12345' },
{ token: '{TvMazeId:12345}', example: '54321' },
{ token: '{Season:00}', example: '01' },
{ token: '{Episode:00}', example: '01' }

View File

@@ -21,6 +21,7 @@ const requiresRestartKeys = [
'bindAddress',
'port',
'urlBase',
'instanceName',
'enableSsl',
'sslPort',
'sslCertPath',

View File

@@ -62,6 +62,8 @@ class UISettings extends Component {
...otherProps
} = this.props;
const uiLanguages = languages.filter((item) => item.value !== 'Original');
const themeOptions = Object.keys(themes)
.map((theme) => ({ key: theme, value: titleCase(theme) }));
@@ -170,7 +172,7 @@ class UISettings extends Component {
<FormInputGroup
type={inputTypes.SELECT}
name="uiLanguage"
values={languages}
values={uiLanguages}
helpText={translate('UILanguageHelpText')}
helpTextWarning={translate('UILanguageHelpTextWarning')}
onChange={onInputChange}

View File

@@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchLocalizationOptions } from 'Store/Actions/localizationActions';
import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import UISettings from './UISettings';
@@ -12,19 +11,18 @@ const SECTION = 'ui';
function createLanguagesSelector() {
return createSelector(
(state) => state.localization,
(localization) => {
console.log(localization);
const items = localization.items;
(state) => state.settings.languages,
(languages) => {
const items = languages.items;
const filterItems = ['Any', 'Unknown'];
if (!items) {
return [];
}
const newItems = items.filter((lang) => !items.includes(lang.name)).map((item) => {
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => {
return {
key: item.value,
key: item.id,
value: item.name
};
});
@@ -53,7 +51,6 @@ const mapDispatchToProps = {
setUISettingsValue,
saveUISettings,
fetchUISettings,
fetchLocalizationOptions,
clearPendingChanges
};
@@ -64,7 +61,6 @@ class UISettingsConnector extends Component {
componentDidMount() {
this.props.fetchUISettings();
this.props.fetchLocalizationOptions();
}
componentWillUnmount() {
@@ -100,7 +96,6 @@ UISettingsConnector.propTypes = {
setUISettingsValue: PropTypes.func.isRequired,
saveUISettings: PropTypes.func.isRequired,
fetchUISettings: PropTypes.func.isRequired,
fetchLocalizationOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};

View File

@@ -0,0 +1,48 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.languages';
//
// Actions Types
export const FETCH_LANGUAGES = 'settings/languages/fetchLanguages';
//
// Action Creators
export const fetchLanguages = createThunk(FETCH_LANGUAGES);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_LANGUAGES]: createFetchHandler(section, '/language')
},
//
// Reducers
reducers: {
}
};

View File

@@ -36,31 +36,31 @@ export const defaultState = {
},
{
name: 'indexer',
label: translate('Indexer'),
label: 'Indexer',
isSortable: false,
isVisible: true
},
{
name: 'query',
label: translate('Query'),
label: 'Query',
isSortable: false,
isVisible: true
},
{
name: 'parameters',
label: translate('Parameters'),
label: 'Parameters',
isSortable: false,
isVisible: false
},
{
name: 'grabTitle',
label: translate('Grab Title'),
label: 'Grab Title',
isSortable: false,
isVisible: false
},
{
name: 'categories',
label: translate('Categories'),
label: 'Categories',
isSortable: false,
isVisible: true
},
@@ -72,13 +72,13 @@ export const defaultState = {
},
{
name: 'source',
label: translate('Source'),
label: 'Source',
isSortable: false,
isVisible: false
},
{
name: 'elapsedTime',
label: translate('Elapsed Time'),
label: 'Elapsed Time',
isSortable: false,
isVisible: true
},

View File

@@ -7,7 +7,6 @@ import * as indexers from './indexerActions';
import * as indexerIndex from './indexerIndexActions';
import * as indexerStats from './indexerStatsActions';
import * as indexerStatus from './indexerStatusActions';
import * as localization from './localizationActions';
import * as oAuth from './oAuthActions';
import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions';
@@ -26,7 +25,6 @@ export default [
paths,
providerOptions,
releases,
localization,
indexers,
indexerIndex,
indexerStats,

View File

@@ -36,7 +36,7 @@ export const defaultState = {
columns: [
{
name: 'select',
columnLabel: translate('Select'),
columnLabel: 'Select',
isSortable: false,
isVisible: true,
isModifiable: false,
@@ -51,7 +51,7 @@ export const defaultState = {
},
{
name: 'sortName',
label: translate('IndexerName'),
label: 'Indexer Name',
isSortable: true,
isVisible: true,
isModifiable: false
@@ -88,7 +88,7 @@ export const defaultState = {
},
{
name: 'capabilities',
label: translate('Categories'),
label: 'Categories',
isSortable: false,
isVisible: true
},

View File

@@ -1,39 +0,0 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk, handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'localization';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_LOCALIZATION_OPTIONS = 'localization/fetchLocalizationOptions';
//
// Action Creators
export const fetchLocalizationOptions = createThunk(FETCH_LOCALIZATION_OPTIONS);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_LOCALIZATION_OPTIONS]: createFetchHandler(section, '/localization/options')
});
//
// Reducers
export const reducers = createHandleActions({}, defaultState, section);

View File

@@ -8,6 +8,7 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages';
import notifications from './Settings/notifications';
import ui from './Settings/ui';
@@ -15,6 +16,7 @@ export * from './Settings/downloadClients';
export * from './Settings/general';
export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies';
export * from './Settings/languages';
export * from './Settings/notifications';
export * from './Settings/applications';
export * from './Settings/appProfiles';
@@ -36,6 +38,7 @@ export const defaultState = {
general: general.defaultState,
indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState,
notifications: notifications.defaultState,
applications: applications.defaultState,
appProfiles: appProfiles.defaultState,
@@ -65,6 +68,7 @@ export const actionHandlers = handleThunks({
...general.actionHandlers,
...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers,
...languages.actionHandlers,
...notifications.actionHandlers,
...applications.actionHandlers,
...appProfiles.actionHandlers,
@@ -85,6 +89,7 @@ export const reducers = createHandleActions({
...general.reducers,
...indexerCategories.reducers,
...indexerProxies.reducers,
...languages.reducers,
...notifications.reducers,
...applications.reducers,
...appProfiles.reducers,

View File

@@ -168,11 +168,10 @@ module.exports = {
//
// Popover
popoverTitleBackgroundColor: '#424242',
popoverTitleBorderColor: '#2a2a2a',
popoverBodyBackgroundColor: '#2a2a2a',
popoverTitleBackgroundColor: '#f7f7f7',
popoverTitleBorderColor: '#ebebeb',
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
popoverArrowBorderColor: '#2a2a2a',
popoverArrowBorderColor: '#fff',
popoverTitleBackgroundInverseColor: '#595959',
popoverTitleBorderInverseColor: '#707070',

View File

@@ -170,7 +170,6 @@ module.exports = {
popoverTitleBackgroundColor: '#f7f7f7',
popoverTitleBorderColor: '#ebebeb',
popoverBodyBackgroundColor: '#e9e9e9',
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
popoverArrowBorderColor: '#fff',

View File

@@ -13,7 +13,7 @@ class Donations extends Component {
return (
<FieldSet legend={translate('Donations')}>
<div className={styles.logoContainer} title="Radarr">
<Link to="https://radarr.video/donate">
<Link to="https://opencollective.com/radarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
@@ -21,7 +21,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Lidarr">
<Link to="https://lidarr.audio/donate">
<Link to="https://opencollective.com/lidarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
@@ -29,7 +29,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Readarr">
<Link to="https://readarr.com/donate">
<Link to="https://opencollective.com/readarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
@@ -37,7 +37,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Prowlarr">
<Link to="https://prowlarr.com/donate">
<Link to="https://opencollective.com/prowlarr">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}

View File

@@ -15,27 +15,27 @@ const columns = [
},
{
name: 'commandName',
label: translate('Name'),
label: 'Name',
isVisible: true
},
{
name: 'queued',
label: translate('Queued'),
label: 'Queued',
isVisible: true
},
{
name: 'started',
label: translate('Started'),
label: 'Started',
isVisible: true
},
{
name: 'ended',
label: translate('Ended'),
label: 'Ended',
isVisible: true
},
{
name: 'duration',
label: translate('Duration'),
label: 'Duration',
isVisible: true
},
{

View File

@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [
{
name: 'name',
label: translate('Name'),
label: 'Name',
isVisible: true
},
{
name: 'interval',
label: translate('Interval'),
label: 'Interval',
isVisible: true
},
{
name: 'lastExecution',
label: translate('LastExecution'),
label: 'Last Execution',
isVisible: true
},
{
name: 'lastDuration',
label: translate('LastDuration'),
label: 'Last Duration',
isVisible: true
},
{
name: 'nextExecution',
label: translate('NextExecution'),
label: 'Next Execution',
isVisible: true
},
{

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.18",
"@microsoft/signalr": "6.0.6",
"@microsoft/signalr": "6.0.3",
"@sentry/browser": "6.19.2",
"@sentry/integrations": "6.19.2",
"chart.js": "3.7.1",
@@ -78,38 +78,38 @@
"reselect": "4.0.0"
},
"devDependencies": {
"@babel/core": "7.18.2",
"@babel/eslint-parser": "7.18.2",
"@babel/plugin-proposal-class-properties": "7.17.12",
"@babel/plugin-proposal-decorators": "7.18.2",
"@babel/plugin-proposal-export-default-from": "7.17.12",
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
"@babel/plugin-proposal-function-sent": "7.18.2",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
"@babel/core": "7.17.8",
"@babel/eslint-parser": "7.17.0",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-decorators": "7.17.8",
"@babel/plugin-proposal-export-default-from": "7.16.7",
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
"@babel/plugin-proposal-function-sent": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-numeric-separator": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.17.12",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-throw-expressions": "7.16.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.18.2",
"@babel/preset-react": "7.17.12",
"autoprefixer": "10.4.7",
"babel-loader": "8.2.5",
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"autoprefixer": "10.4.4",
"babel-loader": "8.2.4",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.22.8",
"core-js": "3.21.1",
"css-loader": "6.7.1",
"eslint": "8.17.0",
"eslint": "8.11.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-react": "7.30.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "3.6.0",
"esprint": "3.3.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "6.1.7",
"html-webpack-plugin": "5.5.0",
"loader-utils": "^3.0.0",
"mini-css-extract-plugin": "2.6.0",
"postcss": "8.4.14",
"postcss": "8.4.12",
"postcss-color-function": "4.1.0",
"postcss-loader": "6.2.1",
"postcss-mixins": "9.0.2",
@@ -121,10 +121,10 @@
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.1",
"stylelint": "14.8.5",
"stylelint": "14.6.0",
"stylelint-order": "5.0.0",
"url-loader": "4.1.1",
"webpack": "5.73.0",
"webpack": "5.70.0",
"webpack-cli": "4.9.2",
"webpack-livereload-plugin": "3.0.2"
}

View File

@@ -94,7 +94,7 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
_runner.KillAll();
_runner.Start();

View File

@@ -10,16 +10,6 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
where TSubject : class, IDiskProvider
{
[Test]
public void writealltext_should_truncate_existing()
{
var file = GetTempFilePath();
Subject.WriteAllText(file, "A pretty long string");
Subject.WriteAllText(file, "A short string");
Subject.ReadAllText(file).Should().Be("A short string");
}
[Test]
[Retry(5)]
public void directory_exist_should_be_able_to_find_existing_folder()

View File

@@ -402,40 +402,6 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(source.FullName, destination.FullName);
}
[Test]
public void CopyFolder_should_detect_caseinsensitive_parents()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "a/series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_detect_caseinsensitive_folder()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_ignore_nfs_temp_file()
{
@@ -485,42 +451,6 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyMoveFolder(original.FullName, source.FullName, destination.FullName);
}
[Test]
public void MoveFolder_should_detect_caseinsensitive_parents()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "a/series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move));
}
[Test]
public void MoveFolder_should_rename_caseinsensitive_folder()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
source.FullName.GetActualCasing().Should().Be(destination.FullName);
}
[Test]
public void should_throw_if_destination_is_readonly()
{
@@ -623,23 +553,6 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(original.FullName, destination.FullName);
}
[Test]
public void MirrorFolder_should_handle_trailing_slash()
{
WithRealDiskProvider();
var original = GetFilledTempFolder();
var source = new DirectoryInfo(GetTempFilePath());
var destination = new DirectoryInfo(GetTempFilePath());
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
var count = Subject.MirrorFolder(source.FullName + Path.DirectorySeparatorChar, destination.FullName);
count.Should().Equals(3);
VerifyCopyFolder(original.FullName, destination.FullName);
}
[Test]
public void TransferFolder_should_use_movefolder_if_on_same_mount()
{
@@ -839,10 +752,6 @@ namespace NzbDrone.Common.Test.DiskTests
.Setup(v => v.CreateFolder(It.IsAny<string>()))
.Callback<string>(v => Directory.CreateDirectory(v));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.MoveFolder(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
.Callback<string, string, bool>((v, r, b) => Directory.Move(v, r));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.DeleteFolder(It.IsAny<string>(), It.IsAny<bool>()))
.Callback<string, bool>((v, r) => Directory.Delete(v, r));

View File

@@ -28,6 +28,14 @@ namespace NzbDrone.Common.Test.DiskTests
Subject.GetAvailableSpace(Path.Combine(path, "invalidFolder")).Should().NotBe(0);
}
[Ignore("Docker")]
[Test]
public void should_be_able_to_check_space_on_ramdrive()
{
PosixOnly();
Subject.GetAvailableSpace("/run/").Should().NotBe(0);
}
[Ignore("Docker")]
[Test]
public void should_return_free_disk_space()
@@ -36,6 +44,35 @@ namespace NzbDrone.Common.Test.DiskTests
result.Should().BeGreaterThan(0);
}
[Test]
public void should_be_able_to_get_space_on_unc()
{
WindowsOnly();
var result = Subject.GetAvailableSpace(@"\\localhost\c$\Windows");
result.Should().BeGreaterThan(0);
}
[Test]
public void should_throw_if_drive_doesnt_exist()
{
WindowsOnly();
// Find a drive that doesn't exist.
for (char driveletter = 'Z'; driveletter > 'D'; driveletter--)
{
if (new DriveInfo(driveletter.ToString()).IsReady)
{
continue;
}
Assert.Throws<DirectoryNotFoundException>(() => Subject.GetAvailableSpace(driveletter + @":\NOT_A_REAL_PATH\DOES_NOT_EXIST".AsOsAgnostic()));
return;
}
Assert.Inconclusive("No drive available for testing.");
}
[Ignore("Docker")]
[Test]
public void should_be_able_to_get_space_on_folder_that_doesnt_exist()

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,22 +46,31 @@ 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.TooManyRequests)
if (res == null || res.StatusCode != (HttpStatusCode)429)
{
return false;
}
@@ -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)]
@@ -241,28 +190,6 @@ namespace NzbDrone.Common.Test.Http
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_throw_on_suppressed_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test]
public void should_not_follow_redirects_when_not_in_production()
{
@@ -388,38 +315,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()
{
@@ -851,7 +753,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

@@ -28,12 +28,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Indexer and Download Client Responses
// avistaz response
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
// animebytes response
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
// danish bytes response
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
@@ -80,24 +77,20 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Download Station
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
// Tracker Responses
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// BroadcastheNet
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
[TestCase(@"getTorrents(""mySecret"", [asdfasdf], 100, 0)")]
[TestCase(@"""DownloadURL"":""https://broadcasthe.net/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
[TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
// Webhooks - Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/prowlarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
// Notifiarr
[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")]
// RSS
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&amp;extended=1&amp;cat=3030&apikey=mySecret&amp;q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
// Internal
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
public void should_clean_message(string message)
{

View File

@@ -170,7 +170,7 @@ namespace NzbDrone.Common.Test
var processStarted = new ManualResetEventSlim();
string suffix;
if (OsInfo.IsWindows)
if (OsInfo.IsWindows || PlatformInfo.IsMono)
{
suffix = ".exe";
}

View File

@@ -4,13 +4,11 @@ using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
@@ -31,8 +29,7 @@ namespace NzbDrone.Common.Test
.AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
var serviceProvider = container.GetServiceProvider();

View File

@@ -30,8 +30,7 @@ namespace NzbDrone.Common.Disk
public abstract long? GetAvailableSpace(string path);
public abstract void InheritFolderPermissions(string filename);
public abstract void SetEveryonePermissions(string filename);
public abstract void SetFilePermissions(string path, string mask, string group);
public abstract void SetPermissions(string path, string mask, string group);
public abstract void SetPermissions(string path, string mask);
public abstract void CopyPermissions(string sourcePath, string targetPath);
public abstract long? GetTotalSize(string path);
@@ -131,7 +130,7 @@ namespace NzbDrone.Common.Disk
{
var testPath = Path.Combine(path, "prowlarr_write_test.txt");
var testContent = string.Format("This file was created to verify if '{0}' is writable. It should've been automatically deleted. Feel free to delete it.", path);
WriteAllText(testPath, testContent);
File.WriteAllText(testPath, testContent);
File.Delete(testPath);
return true;
}
@@ -259,6 +258,17 @@ namespace NzbDrone.Common.Disk
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
if (source.PathEquals(destination))
{
throw new IOException(string.Format("Source and destination can't be the same {0}", source));
}
if (FolderExists(destination) && overwrite)
{
DeleteFolder(destination, true);
}
RemoveReadOnlyFolder(source);
Directory.Move(source, destination);
}
@@ -300,16 +310,7 @@ namespace NzbDrone.Common.Disk
{
Ensure.That(filename, () => filename).IsValidPath();
RemoveReadOnly(filename);
// File.WriteAllText is broken on net core when writing to some CIFS mounts
// This workaround from https://github.com/dotnet/runtime/issues/42790#issuecomment-700362617
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (var writer = new StreamWriter(fs))
{
writer.Write(contents);
}
}
File.WriteAllText(filename, contents);
}
public void FolderSetLastWriteTime(string path, DateTime dateTime)
@@ -549,7 +550,7 @@ namespace NzbDrone.Common.Disk
}
}
public virtual bool IsValidFolderPermissionMask(string mask)
public virtual bool IsValidFilePermissionMask(string mask)
{
throw new NotSupportedException();
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Disk
@@ -26,56 +27,11 @@ namespace NzbDrone.Common.Disk
_logger = logger;
}
private string ResolveRealParentPath(string path)
{
var parentPath = path.GetParentPath();
if (!_diskProvider.FolderExists(parentPath))
{
return path;
}
var realParentPath = parentPath.GetActualCasing();
var partialChildPath = path.Substring(parentPath.Length);
return realParentPath + partialChildPath;
}
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
{
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("{0} Directory [{1}] > [{2}]", mode, sourcePath, targetPath);
if (sourcePath == targetPath)
{
throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath));
}
if (mode == TransferMode.Move && sourcePath.PathEquals(targetPath, StringComparison.InvariantCultureIgnoreCase) && _diskProvider.FolderExists(targetPath))
{
// Move folder out of the way to allow case-insensitive renames
var tempPath = sourcePath + ".backup~";
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", sourcePath, tempPath);
_diskProvider.MoveFolder(sourcePath, tempPath);
if (!_diskProvider.FolderExists(targetPath))
{
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, targetPath);
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
_diskProvider.MoveFolder(tempPath, targetPath);
return mode;
}
// There were two separate folders, revert the intermediate rename and let the recursion deal with it
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, sourcePath);
_diskProvider.MoveFolder(tempPath, sourcePath);
}
if (mode == TransferMode.Move && !_diskProvider.FolderExists(targetPath))
{
var sourceMount = _diskProvider.GetMount(sourcePath);
@@ -84,7 +40,7 @@ namespace NzbDrone.Common.Disk
// If we're on the same mount, do a simple folder move.
if (sourceMount != null && targetMount != null && sourceMount.RootDirectory == targetMount.RootDirectory)
{
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
_logger.Debug("Move Directory [{0}] > [{1}]", sourcePath, targetPath);
_diskProvider.MoveFolder(sourcePath, targetPath);
return mode;
}
@@ -123,13 +79,6 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move))
{
var totalSize = _diskProvider.GetFileInfos(sourcePath).Sum(v => v.Length);
if (totalSize > (100 * 1024L * 1024L))
{
throw new IOException($"Large files still exist in {sourcePath} after folder move, not deleting source folder");
}
_diskProvider.DeleteFolder(sourcePath, true);
}
@@ -143,10 +92,7 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("Mirror Folder [{0}] > [{1}]", sourcePath, targetPath);
_logger.Debug("Mirror [{0}] > [{1}]", sourcePath, targetPath);
if (!_diskProvider.FolderExists(targetPath))
{
@@ -258,9 +204,6 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
var originalSize = _diskProvider.GetFileSize(sourcePath);

View File

@@ -11,8 +11,7 @@ namespace NzbDrone.Common.Disk
long? GetAvailableSpace(string path);
void InheritFolderPermissions(string filename);
void SetEveryonePermissions(string filename);
void SetFilePermissions(string path, string mask, string group);
void SetPermissions(string path, string mask, string group);
void SetPermissions(string path, string mask);
void CopyPermissions(string sourcePath, string targetPath);
long? GetTotalSize(string path);
DateTime FolderGetCreationTime(string path);
@@ -57,6 +56,6 @@ namespace NzbDrone.Common.Disk
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
void RemoveEmptySubfolders(string path);
void SaveStream(Stream stream, string path);
bool IsValidFolderPermissionMask(string mask);
bool IsValidFilePermissionMask(string mask);
}
}

View File

@@ -1,79 +1,15 @@
using System;
using System.IO;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Disk
{
public static class LongPathSupport
{
private static int MAX_PATH;
private static int MAX_NAME;
public static void Enable()
{
// Mono has an issue with enabling long path support via app.config.
// This works for both mono and .net on Windows.
AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
DetectLongPathLimits();
}
private static void DetectLongPathLimits()
{
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_PATH"), out MAX_PATH))
{
if (OsInfo.IsLinux)
{
MAX_PATH = 4096;
}
else
{
try
{
// Windows paths can be up to 32,767 characters long, but each component of the path must be less than 255.
// If the OS does not have Long Path enabled, then the following will throw an exception
// ref: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
Path.GetDirectoryName($@"C:\{new string('a', 254)}\{new string('a', 254)}");
MAX_PATH = 4096;
}
catch
{
MAX_PATH = 260 - 1;
}
}
}
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_NAME"), out MAX_NAME))
{
MAX_NAME = 255;
}
}
public static int MaxFilePathLength
{
get
{
if (MAX_PATH == 0)
{
DetectLongPathLimits();
}
return MAX_PATH;
}
}
public static int MaxFileNameLength
{
get
{
if (MAX_NAME == 0)
{
DetectLongPathLimits();
}
return MAX_NAME;
}
}
}
}

View File

@@ -210,26 +210,5 @@ namespace NzbDrone.Common.Extensions
return result.TrimStart(' ', '.').TrimEnd(' ');
}
public static string EncodeRFC3986(this string value)
{
// From Twitterizer http://www.twitterizer.net/
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var encoded = Uri.EscapeDataString(value);
return Regex
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
}
}
}

View File

@@ -1,12 +0,0 @@
using System.Net;
namespace NzbDrone.Common.Http
{
public class BasicNetworkCredential : NetworkCredential
{
public BasicNetworkCredential(string user, string pass)
: base(user, pass)
{
}
}
}

View File

@@ -10,7 +10,6 @@ namespace NzbDrone.Common.Http
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
// NOTE: we are not checking non-ascii characters and we should
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
@@ -25,7 +24,7 @@ namespace NzbDrone.Common.Http
var matches = _CookieRegex.Match(cookieHeader);
while (matches.Success)
{
if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))
if (matches.Groups.Count > 2)
{
cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value;
}

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

@@ -6,5 +6,6 @@ namespace NzbDrone.Common.Http.Dispatchers
public interface IHttpDispatcher
{
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
Task DownloadFileAsync(string url, string fileName);
}
}

View File

@@ -1,16 +1,13 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Reflection;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
@@ -18,223 +15,221 @@ 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 ICached<CredentialCache> _credentialCache;
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));
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
_platformInfo = platformInfo;
_logger = logger;
}
public async Task<HttpResponse> GetResponseAsync(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.Credentials != null)
{
if (request.Credentials is BasicNetworkCredential bc)
{
// Manually set header to avoid initial challenge response
var authInfo = bc.UserName + ":" + bc.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
requestMessage.Headers.Add("Authorization", "Basic " + authInfo);
}
else if (request.Credentials is NetworkCredential nc)
{
var creds = GetCredentialCache();
foreach (var authtype in new[] { "Basic", "Digest" })
{
creds.Remove((Uri)request.Url, authtype);
creds.Add((Uri)request.Url, authtype, nc);
}
}
}
if (request.ContentData != null)
{
requestMessage.Content = new ByteArrayContent(request.ContentData);
}
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url);
if (request.Headers != null)
{
AddRequestHeaders(requestMessage, request.Headers);
AddRequestHeaders(webRequest, request.Headers);
}
var httpClient = GetClient(request.Url);
HttpWebResponse httpWebResponse;
var sw = new Stopwatch();
sw.Start();
using var responseMessage = await httpClient.SendAsync(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)await webRequest.GetResponseAsync();
}
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);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
CookieContainer responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{
foreach (var responseCookieHeader in cookieHeaders)
try
{
try
{
cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
}
catch
{
// Ignore invalid cookies
}
data = await responseStream.ToBytes();
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
}
}
}
var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
sw.Stop();
sw.Stop();
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), httpWebResponse.Cookies, data, sw.ElapsedMilliseconds, httpWebResponse.StatusCode);
}
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
public async Task DownloadFileAsync(string url, string fileName)
{
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();
var uri = new HttpUri(url);
using (var webClient = new GZipWebClient())
{
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.Proxy = GetProxy(uri);
await webClient.DownloadFileTaskAsync(url, 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);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
throw;
}
catch (Exception e)
{
_logger.Warn(e, "Failed to get response from: " + url);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
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,
Credentials = GetCredentialCache(),
PreAuthenticate = true,
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":
webRequest.Headers.UserAgent.ParseAdd(header.Value);
webRequest.UserAgent = header.Value;
break;
case "Proxy-Connection":
throw new NotImplementedException();
@@ -244,84 +239,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 CredentialCache GetCredentialCache()
{
return _credentialCache.Get("credentialCache", () => new CredentialCache());
}
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,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -87,21 +86,13 @@ namespace NzbDrone.Common.Http
}
// 302 or 303 should default to GET on redirect even if POST on original
if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod)
{
request.Method = HttpMethod.Get;
request.ContentData = null;
}
// Save to add to final response
var responseCookies = response.Cookies;
// Update cookiecontainer for next request with any cookies recieved on last request
var responseContainer = HandleRedirectCookies(request, response);
response = await ExecuteRequestAsync(request, responseContainer);
response.Cookies.Add(responseCookies);
response = await ExecuteRequestAsync(request, cookieContainer);
}
while (response.HasHttpRedirect);
}
@@ -111,14 +102,11 @@ namespace NzbDrone.Common.Http
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
}
if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
if (!request.SuppressHttpError && response.HasHttpError)
{
if (request.LogHttpError)
{
_logger.Warn("HTTP Error - {0}", response);
}
_logger.Warn("HTTP Error - {0}", response);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
if ((int)response.StatusCode == 429)
{
throw new TooManyRequestsException(request, response);
}
@@ -136,21 +124,6 @@ namespace NzbDrone.Common.Http
return ExecuteAsync(request).GetAwaiter().GetResult();
}
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
{
switch (statusCode)
{
case HttpStatusCode.Moved:
case HttpStatusCode.Found:
case HttpStatusCode.MultipleChoices:
return requestMethod == HttpMethod.Post;
case HttpStatusCode.SeeOther:
return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head;
default:
return false;
}
}
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
{
foreach (var interceptor in _requestInterceptors)
@@ -167,6 +140,8 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew();
PrepareRequestCookies(request, cookieContainer);
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
HandleResponseCookies(response, cookieContainer);
@@ -233,125 +208,52 @@ namespace NzbDrone.Common.Http
}
}
private CookieContainer HandleRedirectCookies(HttpRequest request, HttpResponse response)
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer)
{
var sourceContainer = new CookieContainer();
var responseCookies = response.GetCookies();
if (responseCookies.Count != 0)
// Don't collect persistnet cookies for intermediate/redirected urls.
/*lock (_cookieContainerCache)
{
foreach (var pair in responseCookies)
{
Cookie cookie;
if (pair.Value == null)
{
cookie = new Cookie(pair.Key, "", "/")
{
Expires = DateTime.Now.AddDays(-1)
};
}
else
{
cookie = new Cookie(pair.Key, pair.Value, "/")
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
};
}
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
sourceContainer.Add((Uri)request.Url, cookie);
}
}
return sourceContainer;
cookieContainer.Add(persistentCookies);
cookieContainer.Add(existingCookies);
}*/
}
private void HandleResponseCookies(HttpResponse response, CookieContainer container)
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer)
{
foreach (Cookie cookie in container.GetAllCookies())
{
cookie.Expired = true;
}
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 async Task DownloadFileAsync(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 = await GetAsync(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);
}
}
await _httpDispatcher.DownloadFileAsync(url, fileName);
}
public void DownloadFile(string url, string fileName)

View File

@@ -4,27 +4,11 @@ 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()
{
}
@@ -128,30 +107,6 @@ namespace NzbDrone.Common.Http
}
}
public string ContentEncoding
{
get
{
return GetSingleValue("Content-Encoding");
}
set
{
SetSingleValue("Content-Encoding", value);
}
}
public string Vary
{
get
{
return GetSingleValue("Vary");
}
set
{
SetSingleValue("Vary", value);
}
}
public string UserAgent
{
get

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
@@ -13,13 +12,11 @@ namespace NzbDrone.Common.Http
{
public HttpRequest(string url, HttpAccept httpAccept = null)
{
Method = HttpMethod.Get;
Url = new HttpUri(url);
Headers = new HttpHeader();
Method = HttpMethod.Get;
ConnectionKeepAlive = true;
AllowAutoRedirect = true;
LogHttpError = true;
Cookies = new Dictionary<string, string>();
if (!RuntimeInfo.IsProduction)
@@ -40,20 +37,16 @@ namespace NzbDrone.Common.Http
public IWebProxy Proxy { get; set; }
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public ICredentials Credentials { get; set; }
public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; }
public bool LogHttpError { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString()
{
@@ -110,5 +103,12 @@ namespace NzbDrone.Common.Http
return encoding.GetString(ContentData);
}
}
public void AddBasicAuthentication(string username, string password)
{
var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));
Headers.Set("Authorization", "Basic " + authInfo);
}
}
}

View File

@@ -21,13 +21,12 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
public TimeSpan RateLimit { get; set; }
public bool LogResponseContent { get; set; }
public ICredentials NetworkCredential { get; set; }
public NetworkCredential NetworkCredential { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }
@@ -47,7 +46,6 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader();
Cookies = new Dictionary<string, string>();
FormData = new List<HttpFormData>();
LogHttpError = true;
}
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
@@ -108,7 +106,6 @@ namespace NzbDrone.Common.Http
request.Method = Method;
request.Encoding = Encoding;
request.SuppressHttpError = SuppressHttpError;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect;
request.StoreRequestCookie = StoreRequestCookie;
@@ -116,7 +113,13 @@ namespace NzbDrone.Common.Http
request.ConnectionKeepAlive = ConnectionKeepAlive;
request.RateLimit = RateLimit;
request.LogResponseContent = LogResponseContent;
request.Credentials = NetworkCredential;
if (NetworkCredential != null)
{
var authInfo = NetworkCredential.UserName + ":" + NetworkCredential.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
request.Headers.Set("Authorization", "Basic " + authInfo);
}
foreach (var header in Headers)
{
@@ -209,7 +212,7 @@ namespace NzbDrone.Common.Http
}
else
{
summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.GetString(formData.ContentData));
summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.UTF8.GetString(formData.ContentData));
}
}
@@ -229,9 +232,9 @@ namespace NzbDrone.Common.Http
}
else
{
var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.GetString(v.ContentData))));
var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.UTF8.GetString(v.ContentData))));
var urlencoded = string.Join("&", parameters);
var body = Encoding.GetBytes(urlencoded);
var body = Encoding.UTF8.GetBytes(urlencoded);
request.Headers.ContentType = "application/x-www-form-urlencoded";
request.SetContent(body);
@@ -403,7 +406,7 @@ namespace NzbDrone.Common.Http
FormData.Add(new HttpFormData
{
Name = key,
ContentData = Encoding.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
});
return this;

View File

@@ -64,11 +64,10 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400;
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.SeeOther ||
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.MultipleChoices ||
StatusCode == HttpStatusCode.PermanentRedirect ||
StatusCode == HttpStatusCode.Found ||
Headers.ContainsKey("Refresh");
public string RedirectUrl
@@ -118,7 +117,7 @@ namespace NzbDrone.Common.Http
public override string ToString()
{
var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
var result = string.Format("Res: [{0}] {1}: {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode);
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
{

View File

@@ -1,103 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Http
{
public class XmlRpcRequestBuilder : HttpRequestBuilder
{
public static string XmlRpcContentType = "text/xml";
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(XmlRpcRequestBuilder));
public string XmlMethod { get; private set; }
public List<object> XmlParameters { get; private set; }
public XmlRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.Post;
XmlParameters = new List<object>();
}
public XmlRpcRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
: this(BuildBaseUrl(useHttps, host, port, urlBase))
{
}
public override HttpRequestBuilder Clone()
{
var clone = base.Clone() as XmlRpcRequestBuilder;
clone.XmlParameters = new List<object>(XmlParameters);
return clone;
}
public XmlRpcRequestBuilder Call(string method, params object[] parameters)
{
var clone = Clone() as XmlRpcRequestBuilder;
clone.XmlMethod = method;
clone.XmlParameters = parameters.ToList();
return clone;
}
protected override void Apply(HttpRequest request)
{
base.Apply(request);
request.Headers.ContentType = XmlRpcContentType;
var methodCallElements = new List<XElement> { new XElement("methodName", XmlMethod) };
if (XmlParameters.Any())
{
var argElements = XmlParameters.Select(x => new XElement("param", ConvertParameter(x))).ToList();
var paramsElement = new XElement("params", argElements);
methodCallElements.Add(paramsElement);
}
var message = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("methodCall", methodCallElements));
var body = message.ToString();
Logger.Debug($"Executing remote method: {XmlMethod}");
Logger.Trace($"methodCall {XmlMethod} body:\n{body}");
request.SetContent(body);
}
private static XElement ConvertParameter(object value)
{
XElement data;
if (value is string s)
{
data = new XElement("string", s);
}
else if (value is List<string> l)
{
data = new XElement("array", new XElement("data", l.Select(x => new XElement("value", new XElement("string", x)))));
}
else if (value is int i)
{
data = new XElement("int", i);
}
else if (value is byte[] bytes)
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
}
return new XElement("value", data);
}
}
}

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -28,9 +28,6 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// NzbGet
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -54,8 +51,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Indexer Responses
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}\\\/rss\\\/download\\\/(?<secret>[^&=]+?)\\\/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using NLog;
using NLog.Fluent;
@@ -10,46 +8,47 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
{
return logBuilder.Property("Sentry", fingerprint);
}
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
}
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
}
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
}
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
}
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
{
SentryLogger.ForLogEvent(level)
.CopyLogEvent(logBuilder.LogEvent)
SentryLogger.Log(level)
.CopyLogEvent(logBuilder.LogEventInfo)
.SentryFingerprint(fingerprint)
.Log();
.Write();
return logBuilder.Property<string>("Sentry", null);
return logBuilder.Property("Sentry", null);
}
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
{
return logBuilder.TimeStamp(logEvent.TimeStamp)
return logBuilder.LoggerName(logEvent.LoggerName)
.TimeStamp(logEvent.TimeStamp)
.Message(logEvent.Message, logEvent.Parameters)
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
.Exception(logEvent.Exception);
}
}

View File

@@ -1,16 +1,13 @@
using System;
using System.Text;
using NLog;
using NLog;
using NLog.Targets;
namespace NzbDrone.Common.Instrumentation
{
public class NzbDroneFileTarget : FileTarget
{
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
protected override string GetFormattedMessage(LogEventInfo logEvent)
{
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
target.Append(result);
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
}
}
}

View File

@@ -34,8 +34,6 @@ namespace NzbDrone.Common.Instrumentation
var appFolderInfo = new AppFolderInfo(startupContext);
RegisterGlobalFilters();
if (Debugger.IsAttached)
{
RegisterDebugger();
@@ -99,21 +97,10 @@ namespace NzbDrone.Common.Instrumentation
target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
var loggingRule = new LoggingRule("*", LogLevel.Trace, target);
LogManager.Configuration.AddTarget("debugger", target);
LogManager.Configuration.LoggingRules.Add(loggingRule);
}
private static void RegisterGlobalFilters()
{
LogManager.Setup().LoadConfiguration(c =>
{
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
});
}
private static void RegisterConsole()
{
var level = LogLevel.Trace;

View File

@@ -127,18 +127,7 @@ namespace NzbDrone.Common.Processes
try
{
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
var key = environmentVariable.Key.ToString();
var value = environmentVariable.Value?.ToString();
if (startInfo.EnvironmentVariables.ContainsKey(key))
{
startInfo.EnvironmentVariables[key] = value;
}
else
{
startInfo.EnvironmentVariables.Add(key, value);
}
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
}
catch (Exception e)
{

View File

@@ -9,10 +9,8 @@
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.19.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="Sentry" Version="3.15.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore
public void SingleOrDefault_should_return_null_on_empty_db()
{
Mocker.Resolve<IDatabase>()
.OpenConnection().Query<IndexerDefinition>("SELECT * FROM \"Indexers\"")
.OpenConnection().Query<IndexerDefinition>("SELECT * FROM Indexers")
.SingleOrDefault()
.Should()
.BeNull();

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Http;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Framework
@@ -26,8 +25,7 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
}

View File

@@ -5,14 +5,10 @@ using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test.Framework
{
@@ -53,7 +49,6 @@ namespace NzbDrone.Core.Test.Framework
public abstract class DbTest : CoreTest
{
private ITestDatabase _db;
private DatabaseType _databaseType;
protected virtual MigrationType MigrationType => MigrationType.Main;
@@ -106,39 +101,17 @@ namespace NzbDrone.Core.Test.Framework
private IDatabase CreateDatabase(MigrationContext migrationContext)
{
if (_databaseType == DatabaseType.PostgreSQL)
{
CreatePostgresDb();
}
var factory = Mocker.Resolve<DbFactory>();
// If a special migration test or log migration then create new
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
if (migrationContext.BeforeMigration != null)
{
return factory.Create(migrationContext);
}
return CreateSqliteDatabase(factory, migrationContext);
}
private void CreatePostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Create(options, MigrationType);
}
private void DropPostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Drop(options, MigrationType);
}
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
{
// Otherwise try to use a cached migrated db
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
var cachedDb = GetCachedDatabase(migrationContext.MigrationType);
var testDb = GetTestDb(migrationContext.MigrationType);
if (File.Exists(cachedDb))
{
TestLogger.Info($"Using cached initial database {cachedDb}");
@@ -158,7 +131,12 @@ namespace NzbDrone.Core.Test.Framework
}
}
private string GetTestSqliteDb(MigrationType type)
private string GetCachedDatabase(MigrationType type)
{
return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db");
}
private string GetTestDb(MigrationType type)
{
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
}
@@ -173,13 +151,6 @@ namespace NzbDrone.Core.Test.Framework
WithTempAsAppPath();
SetupLogging();
// populate the possible postgres options
var postgresOptions = PostgresDatabase.GetTestOptions();
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
// Set up remaining container services
Mocker.SetConstant(Options.Create(postgresOptions));
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
@@ -200,17 +171,11 @@ namespace NzbDrone.Core.Test.Framework
GC.Collect();
GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools();
NpgsqlConnection.ClearAllPools();
if (TestFolderInfo != null)
{
DeleteTempFolder(TestFolderInfo.AppDataFolder);
}
if (_databaseType == DatabaseType.PostgreSQL)
{
DropPostgresDb();
}
}
}
}

View File

@@ -1,7 +1,5 @@
using System.IO;
using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test
{
@@ -12,13 +10,13 @@ namespace NzbDrone.Core.Test
[OneTimeTearDown]
public void ClearCachedDatabase()
{
var mainCache = SqliteDatabase.GetCachedDb(MigrationType.Main);
var mainCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Main.db");
if (File.Exists(mainCache))
{
File.Delete(mainCache);
}
var logCache = SqliteDatabase.GetCachedDb(MigrationType.Log);
var logCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Log.db");
if (File.Exists(logCache))
{
File.Delete(logCache);

View File

@@ -23,7 +23,6 @@ namespace NzbDrone.Core.Test.Framework
where T : ModelBase, new();
IDirectDataMapper GetDirectDataMapper();
IDbConnection OpenConnection();
DatabaseType DatabaseType { get; }
}
public class TestDatabase : ITestDatabase
@@ -31,8 +30,6 @@ namespace NzbDrone.Core.Test.Framework
private readonly IDatabase _dbConnection;
private readonly IEventAggregator _eventAggregator;
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
public TestDatabase(IDatabase dbConnection)
{
_eventAggregator = new Mock<IEventAggregator>().Object;

View File

@@ -41,8 +41,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Subject.Clean();
// BeCloseTo handles Postgres rounding times
AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().BeCloseTo(expectedTime));
AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().Be(expectedTime));
}
}
}

View File

@@ -17,7 +17,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{
[TestFixture]
public class AvistazFixture : CoreTest<Avistaz>
public class AvistazFixture : CoreTest<AvistaZ>
{
[SetUp]
public void Setup()
@@ -60,10 +60,6 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.ImdbId.Should().Be(15569106);
torrentInfo.TmdbId.Should().Be(135144);
torrentInfo.TvdbId.Should().Be(410548);
torrentInfo.Languages.Should().HaveCount(1);
torrentInfo.Languages.First().Should().Be("Japanese");
torrentInfo.Subs.Should().HaveCount(27);
torrentInfo.Subs.First().Should().Be("Arabic");
}
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Definitions.Avistaz;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{
[TestFixture]
public class ExoticazFixture : CoreTest<ExoticaZ>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "ExoticaZ",
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
};
}
[Test]
public async Task should_parse_recent_feed_from_ExoticaZ()
{
var recentFeed = ReadAllText(@"Files/Indexers/Exoticaz/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(100);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[SSIS-419] My first experience is Yua Mikami. From the day I lost my virginity, I was devoted to sex.");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://exoticaz.to/rss/download/(removed)/(removed).torrent");
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
torrentInfo.Size.Should().Be(7085405541);
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(33);
torrentInfo.Seeders.Should().Be(33);
torrentInfo.Categories.First().Id.Should().Be(6040);
}
}
}

View File

@@ -17,7 +17,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{
[TestFixture]
public class PrivateHDFixture : CoreTest<Avistaz>
public class PrivateHDFixture : CoreTest<PrivateHD>
{
[SetUp]
public void Setup()

View File

@@ -1,91 +0,0 @@
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
{
public class ApplyGoTemplateTextFixture : CoreTest<CardigannBase>
{
private Dictionary<string, object> _variables;
private CardigannDefinition _definition;
[SetUp]
public void SetUp()
{
_variables = new Dictionary<string, object>
{
[".Config.sitelink"] = "https://somesite.com/",
[".True"] = "True",
[".False"] = null,
[".Today.Year"] = DateTime.Today.Year.ToString(),
[".Categories"] = new string[] { "tv", "movies" }
};
_definition = Builder<CardigannDefinition>.CreateNew()
.With(x => x.Encoding = "UTF-8")
.With(x => x.Links = new List<string>
{
"https://somesite.com/"
})
.With(x => x.Caps = new CapabilitiesBlock
{
Modes = new Dictionary<string, List<string>>
{
{ "search", new List<string> { "q" } }
}
})
.Build();
Mocker.SetConstant<CardigannDefinition>(_definition);
}
[TestCase("{{ range .Categories}}&categories[]={{.}}{{end}}", "&categories[]=tv&categories[]=movies")]
[TestCase("{{ range $i, $e := .Categories}}&categories[{{$i}}]={{.}}{{end}}", "&categories[0]=tv&categories[1]=movies")]
[TestCase("{{ range $index, $element := .Categories}}&categories[{{$index}}]={{.}}+postIndex[{{$index}}]{{end}}", "&categories[0]=tv+postIndex[0]&categories[1]=movies+postIndex[1]")]
public void should_handle_range_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ re_replace .Query.Keywords \"[^a-zA-Z0-9]+\" \"%\" }}", "abc%def")]
public void should_handle_re_replace_statements(string template, string expected)
{
_variables[".Query.Keywords"] = string.Join(" ", new List<string> { "abc", "def" });
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ join .Categories \", \" }}", "tv, movies")]
public void should_handle_join_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ .Today.Year }}", "2022")]
public void should_handle_variables_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
[TestCase("{{if .True }}0{{else}}1{{end}}", "0")]
public void should_handle_if_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
}
}

View File

@@ -1,68 +0,0 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
{
[TestFixture]
public class GazelleGamesFixture : CoreTest<GazelleGames>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "GazelleGames",
Settings = new GazelleGamesSettings() { Apikey = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_GazelleGames()
{
var recentFeed = ReadAllText(@"Files/Indexers/GazelleGames/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(1464);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://gazellegames.net/torrents.php?action=download&id=303216&authkey=prowlarr&torrent_pass=");
torrentInfo.InfoUrl.Should().Be("https://gazellegames.net/torrents.php?id=84781&torrentid=303216");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 6:39:11").ToUniversalTime());
torrentInfo.Size.Should().Be(80077617780);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(383);
torrentInfo.Seeders.Should().Be(383);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}

View File

@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
torrents.Should().HaveCount(293);
torrents.First().Should().BeOfType<TorrentInfo>();
torrents.First().Should().BeOfType<PassThePopcornInfo>();
var first = torrents.First() as TorrentInfo;

View File

@@ -3,7 +3,6 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Test.IndexerTests
@@ -21,8 +20,8 @@ namespace NzbDrone.Core.Test.IndexerTests
public int _supportedPageSize;
public override int PageSize => _supportedPageSize;
public TestIndexer(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IIndexerDefinitionUpdateService definitionService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, definitionService, configService, nzbValidationService, logger)
public TestIndexer(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
}

View File

@@ -0,0 +1,85 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Languages
{
[TestFixture]
public class LanguageFixture : CoreTest
{
public static object[] FromIntCases =
{
new object[] { 1, Language.English },
new object[] { 2, Language.French },
new object[] { 3, Language.Spanish },
new object[] { 4, Language.German },
new object[] { 5, Language.Italian },
new object[] { 6, Language.Danish },
new object[] { 7, Language.Dutch },
new object[] { 8, Language.Japanese },
new object[] { 9, Language.Icelandic },
new object[] { 10, Language.Chinese },
new object[] { 11, Language.Russian },
new object[] { 12, Language.Polish },
new object[] { 13, Language.Vietnamese },
new object[] { 14, Language.Swedish },
new object[] { 15, Language.Norwegian },
new object[] { 16, Language.Finnish },
new object[] { 17, Language.Turkish },
new object[] { 18, Language.Portuguese },
new object[] { 19, Language.Flemish },
new object[] { 20, Language.Greek },
new object[] { 21, Language.Korean },
new object[] { 22, Language.Hungarian },
new object[] { 23, Language.Hebrew },
new object[] { 24, Language.Lithuanian },
new object[] { 25, Language.Czech }
};
public static object[] ToIntCases =
{
new object[] { Language.English, 1 },
new object[] { Language.French, 2 },
new object[] { Language.Spanish, 3 },
new object[] { Language.German, 4 },
new object[] { Language.Italian, 5 },
new object[] { Language.Danish, 6 },
new object[] { Language.Dutch, 7 },
new object[] { Language.Japanese, 8 },
new object[] { Language.Icelandic, 9 },
new object[] { Language.Chinese, 10 },
new object[] { Language.Russian, 11 },
new object[] { Language.Polish, 12 },
new object[] { Language.Vietnamese, 13 },
new object[] { Language.Swedish, 14 },
new object[] { Language.Norwegian, 15 },
new object[] { Language.Finnish, 16 },
new object[] { Language.Turkish, 17 },
new object[] { Language.Portuguese, 18 },
new object[] { Language.Flemish, 19 },
new object[] { Language.Greek, 20 },
new object[] { Language.Korean, 21 },
new object[] { Language.Hungarian, 22 },
new object[] { Language.Hebrew, 23 },
new object[] { Language.Lithuanian, 24 },
new object[] { Language.Czech, 25 }
};
[Test]
[TestCaseSource("FromIntCases")]
public void should_be_able_to_convert_int_to_languageTypes(int source, Language expected)
{
var language = (Language)source;
language.Should().Be(expected);
}
[Test]
[TestCaseSource("ToIntCases")]
public void should_be_able_to_convert_languageTypes_to_int(Language source, int expected)
{
var i = (int)source;
i.Should().Be(expected);
}
}
}

View File

@@ -3,6 +3,7 @@ using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
@@ -15,7 +16,7 @@ namespace NzbDrone.Core.Test.Localization
[SetUp]
public void Setup()
{
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns("en");
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.English);
Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory);
}

View File

@@ -1,36 +0,0 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.SecurityTests
{
[TestFixture]
public class ProtectionServiceFixture : CoreTest<ProtectionService>
{
private string _protectionKey;
[SetUp]
public void Setup()
{
_protectionKey = Guid.NewGuid().ToString().Replace("-", "");
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadProtectionKey)
.Returns(_protectionKey);
}
[Test]
public void should_encrypt_and_decrypt_string()
{
const string plainText = "https://prowlarr.com";
var encrypted = Subject.Protect(plainText);
var decrypted = Subject.UnProtect(encrypted);
decrypted.Should().Be(plainText);
}
}
}

View File

@@ -68,10 +68,6 @@ namespace NzbDrone.Core.Test.UpdateTests
.Setup(c => c.FolderWritable(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Returns(true);
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
}
@@ -153,7 +149,7 @@ namespace NzbDrone.Core.Test.UpdateTests
}
[Test]
public void should_start_update_client_if_updater_exists()
public void should_start_update_client()
{
Subject.Execute(new ApplicationUpdateCommand());
@@ -161,21 +157,6 @@ namespace NzbDrone.Core.Test.UpdateTests
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Once());
}
[Test]
public void should_return_with_warning_if_updater_doesnt_exists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Returns(false);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IProcessProvider>()
.Verify(c => c.Start(It.IsAny<string>(), It.IsAny<string>(), null, null, null), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_return_without_error_or_warnings_when_no_updates_are_available()
{

View File

@@ -28,13 +28,10 @@ namespace NzbDrone.Core.Applications.Lidarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -43,8 +40,8 @@ namespace NzbDrone.Core.Applications.Lidarr
var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value);
var seedTimeCompare = seedTime == otherSeedTime;
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var discographySeedTimeCompare = discographySeedTime == otherDiscographySeedTime;
var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
@@ -58,7 +55,7 @@ namespace NzbDrone.Core.Applications.Lidarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
}
}
}

View File

@@ -28,13 +28,10 @@ namespace NzbDrone.Core.Applications.Radarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -54,7 +51,7 @@ namespace NzbDrone.Core.Applications.Radarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
}
}
}

View File

@@ -28,13 +28,10 @@ namespace NzbDrone.Core.Applications.Readarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -43,8 +40,8 @@ namespace NzbDrone.Core.Applications.Readarr
var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value);
var seedTimeCompare = seedTime == otherSeedTime;
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var discographySeedTimeCompare = discographySeedTime == otherDiscographySeedTime;
var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
@@ -58,7 +55,7 @@ namespace NzbDrone.Core.Applications.Readarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
}
}
}

View File

@@ -28,14 +28,11 @@ namespace NzbDrone.Core.Applications.Sonarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var animeCats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "animeCategories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -59,7 +56,7 @@ namespace NzbDrone.Core.Applications.Sonarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
apiKey && apiPath && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
}
}
}

View File

@@ -28,13 +28,10 @@ namespace NzbDrone.Core.Applications.Whisparr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -54,7 +51,7 @@ namespace NzbDrone.Core.Applications.Whisparr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
}
}
}

View File

@@ -5,14 +5,12 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -69,7 +67,6 @@ namespace NzbDrone.Core.Configuration
private readonly IEventAggregator _eventAggregator;
private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly string _configFile;
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -79,14 +76,12 @@ namespace NzbDrone.Core.Configuration
public ConfigFileProvider(IAppFolderInfo appFolderInfo,
ICacheManager cacheManager,
IEventAggregator eventAggregator,
IDiskProvider diskProvider,
IOptions<PostgresOptions> postgresOptions)
IDiskProvider diskProvider)
{
_cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
}
public Dictionary<string, object> GetConfigDictionary()
@@ -200,13 +195,13 @@ namespace NzbDrone.Core.Configuration
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public string PostgresHost => GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public string Theme => GetValue("Theme", "light", persist: false);
public int PostgresPort => GetValueInt("PostgresPort", 5432, persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);

View File

@@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Security;
@@ -138,9 +139,9 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EnableColorImpairedMode", value); }
}
public string UILanguage
public int UILanguage
{
get { return GetValue("UILanguage", "en"); }
get { return GetValueInt("UILanguage", (int)Language.English); }
set { SetValue("UILanguage", value); }
}

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Configuration
string TimeFormat { get; set; }
bool ShowRelativeDates { get; set; }
bool EnableColorImpairedMode { get; set; }
string UILanguage { get; set; }
int UILanguage { get; set; }
//Internal
string PlexClientIdentifier { get; }

View File

@@ -167,12 +167,14 @@ namespace NzbDrone.Core.Datastore
}
}
if (_database.DatabaseType == DatabaseType.PostgreSQL)
if (_database.DatabaseType == DatabaseType.SQLite)
{
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
}
else
{
return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\"";
}
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
}
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)

View File

@@ -0,0 +1,48 @@
using System;
using System.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dapper;
using NzbDrone.Core.Languages;
namespace NzbDrone.Core.Datastore.Converters
{
public class DapperLanguageIntConverter : SqlMapper.TypeHandler<Language>
{
public override void SetValue(IDbDataParameter parameter, Language value)
{
if (value == null)
{
throw new InvalidOperationException("Attempted to save a language that isn't really a language");
}
else
{
parameter.Value = (int)value;
}
}
public override Language Parse(object value)
{
if (value == null || value is DBNull)
{
return Language.Unknown;
}
return (Language)Convert.ToInt32(value);
}
}
public class LanguageIntConverter : JsonConverter<Language>
{
public override Language Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var item = reader.GetInt32();
return (Language)item;
}
public override void Write(Utf8JsonWriter writer, Language value, JsonSerializerOptions options)
{
writer.WriteNumberValue((int)value);
}
}
}

View File

@@ -24,6 +24,7 @@ namespace NzbDrone.Core.Datastore.Migration
Delete.FromTable("Indexers").Row(new { Implementation = "ThePirateBay" });
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentLeech" });
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentSeeds" });
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentParadiseMI" });
Delete.FromTable("Indexers").Row(new { Implementation = "YTS" });
//Change settings to shared classes

View File

@@ -3,7 +3,7 @@ using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(018)]
[Migration(18)]
public class minimum_seeders : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()

View File

@@ -1,15 +0,0 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(019)]
public class remove_showrss : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
// Remove, YML version exists
Delete.FromTable("Indexers").Row(new { Implementation = "ShowRSS" });
}
}
}

View File

@@ -1,15 +0,0 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(020)]
public class remove_torrentparadiseml : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
// Remove, 017 incorrectly removes this using "TorrentParadiseMI"
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentParadiseMl" });
}
}
}

View File

@@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(021)]
public class localization_setting_to_string : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(FixLocalizationConfig);
}
private void FixLocalizationConfig(IDbConnection conn, IDbTransaction tran)
{
string uiLanguage;
string uiCulture;
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Value\" FROM \"Config\" WHERE \"Key\" = 'uilanguage'";
uiLanguage = (string)cmd.ExecuteScalar();
}
if (uiLanguage != null && int.TryParse(uiLanguage, out var uiLanguageInt))
{
uiCulture = _uiMapping.GetValueOrDefault(uiLanguageInt) ?? "en";
using (var insertCmd = conn.CreateCommand())
{
insertCmd.Transaction = tran;
insertCmd.CommandText = string.Format("UPDATE \"Config\" SET \"Value\" = '{0}' WHERE \"Key\" = 'uilanguage'", uiCulture);
insertCmd.ExecuteNonQuery();
}
}
}
private readonly Dictionary<int, string> _uiMapping = new Dictionary<int, string>()
{
{ 1, "en" },
{ 2, "fr" },
{ 3, "es" },
{ 4, "de" },
{ 5, "it" },
{ 6, "da" },
{ 7, "nl" },
{ 8, "ja" },
{ 9, "is" },
{ 10, "zh_CN" },
{ 11, "ru" },
{ 12, "pl" },
{ 13, "vi" },
{ 14, "sv" },
{ 15, "nb_NO" },
{ 16, "fi" },
{ 17, "tr" },
{ 18, "pt" },
{ 19, "en" },
{ 20, "el" },
{ 21, "ko" },
{ 22, "hu" },
{ 23, "he" },
{ 24, "lt" },
{ 25, "cs" },
{ 26, "hi" },
{ 27, "ro" },
{ 28, "th" },
{ 29, "bg" },
{ 30, "pt_BR" },
{ 31, "ar" },
{ 32, "uk" },
{ 33, "fa" },
{ 34, "be" },
{ 35, "zh_TW" },
};
}
}

View File

@@ -1,87 +0,0 @@
using System.Collections.Generic;
using System.Data;
using System.Text.Json;
using Dapper;
using FluentMigrator;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(22)]
public class indexer_definition : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Indexers")
.AddColumn("DefinitionFile").AsString().Nullable();
Execute.WithConnection(MigrateCardigannDefinitions);
}
private void MigrateCardigannDefinitions(IDbConnection conn, IDbTransaction tran)
{
var indexers = new List<Indexer017>();
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Id\", \"Settings\", \"Implementation\", \"ConfigContract\" FROM \"Indexers\"";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
var implementation = reader.GetString(2);
var configContract = reader.GetString(3);
var defFile = implementation.ToLowerInvariant();
if (implementation == "Cardigann")
{
if (!string.IsNullOrWhiteSpace(settings))
{
var jsonObject = STJson.Deserialize<JsonElement>(settings);
if (jsonObject.TryGetProperty("definitionFile", out JsonElement jsonDef))
{
defFile = jsonDef.GetString();
}
}
}
else if (configContract == "AvistazSettings")
{
implementation = "Avistaz";
}
else if (configContract == "Unit3dSettings")
{
implementation = "Unit3d";
}
else if (configContract == "Newznab")
{
defFile = "";
}
indexers.Add(new Indexer017
{
DefinitionFile = defFile,
Implementation = implementation,
Id = id
});
}
}
}
var updateSql = "UPDATE \"Indexers\" SET \"DefinitionFile\" = @DefinitionFile, \"Implementation\" = @Implementation WHERE \"Id\" = @Id";
conn.Execute(updateSql, indexers, transaction: tran);
}
public class Indexer017
{
public int Id { get; set; }
public string DefinitionFile { get; set; }
public string Implementation { get; set; }
}
}
}

View File

@@ -1,26 +0,0 @@
using Microsoft.Extensions.Configuration;
namespace NzbDrone.Core.Datastore
{
public class PostgresOptions
{
public string Host { get; set; }
public int Port { get; set; }
public string User { get; set; }
public string Password { get; set; }
public string MainDb { get; set; }
public string LogDb { get; set; }
public static PostgresOptions GetOptions()
{
var config = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var postgresOptions = new PostgresOptions();
config.GetSection("Prowlarr:Postgres").Bind(postgresOptions);
return postgresOptions;
}
}
}

View File

@@ -14,6 +14,7 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Parser.Model;
@@ -112,6 +113,8 @@ namespace NzbDrone.Core.Datastore
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>());
SqlMapper.AddTypeHandler(new DapperLanguageIntConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<Language>>(new LanguageIntConverter()));
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>());

View File

@@ -313,20 +313,7 @@ namespace NzbDrone.Core.Datastore
_sb.Append(" = ANY (");
// hardcode the integer list if it exists to bypass parameter limit
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
{
var items = (IEnumerable<int>)value;
_sb.Append("('{");
_sb.Append(string.Join(", ", items));
_sb.Append("}')");
_gotConcreteValue = true;
}
else
{
Visit(list);
}
Visit(list);
_sb.Append("))");
}
@@ -337,7 +324,7 @@ namespace NzbDrone.Core.Datastore
Visit(body.Object);
_sb.Append(" ILIKE '%' || ");
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
@@ -350,7 +337,7 @@ namespace NzbDrone.Core.Datastore
Visit(body.Object);
_sb.Append(" ILIKE ");
_sb.Append(" LIKE ");
Visit(body.Arguments[0]);
@@ -363,7 +350,7 @@ namespace NzbDrone.Core.Datastore
Visit(body.Object);
_sb.Append(" ILIKE '%' || ");
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);

View File

@@ -1,161 +1,111 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
using CookComputing.XmlRpc;
namespace NzbDrone.Core.Download.Clients.Aria2
{
public class Aria2Fault
{
public Aria2Fault(XElement element)
{
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").ElementAsInt("int");
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
public int FaultCode { get; set; }
public string FaultString { get; set; }
}
public class Aria2Version
{
public Aria2Version(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
if (e.ElementAsString("name") == "version")
{
Version = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("version")]
public string Version;
public string Version { get; set; }
[XmlRpcMember("enabledFeatures")]
public string[] EnabledFeatures;
}
public class Aria2Uri
{
[XmlRpcMember("status")]
public string Status;
[XmlRpcMember("uri")]
public string Uri;
}
public class Aria2File
{
public Aria2File(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("index")]
public string Index;
if (name == "path")
{
Path = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("length")]
public string Length;
public string Path { get; set; }
}
[XmlRpcMember("completedLength")]
public string CompletedLength;
public class Aria2Dict
{
public Aria2Dict(XElement element)
{
Dict = new Dictionary<string, string>();
[XmlRpcMember("path")]
public string Path;
foreach (var e in element.XPathSelectElements("./struct/member"))
{
Dict.Add(e.ElementAsString("name"), e.Element("value").GetStringValue());
}
}
[XmlRpcMember("selected")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Selected;
public Dictionary<string, string> Dict { get; set; }
}
public class Aria2Bittorrent
{
public Aria2Bittorrent(XElement element)
{
foreach (var e in element.Descendants("member"))
{
if (e.ElementAsString("name") == "name")
{
Name = e.Element("value").GetStringValue();
}
}
}
public string Name;
[XmlRpcMember("uris")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public Aria2Uri[] Uris;
}
public class Aria2Status
{
public Aria2Status(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("bittorrent")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public XmlRpcStruct Bittorrent;
if (name == "bittorrent")
{
Bittorrent = new Aria2Bittorrent(e.Element("value"));
}
else if (name == "infoHash")
{
InfoHash = e.Element("value").GetStringValue();
}
else if (name == "completedLength")
{
CompletedLength = e.Element("value").GetStringValue();
}
else if (name == "downloadSpeed")
{
DownloadSpeed = e.Element("value").GetStringValue();
}
else if (name == "files")
{
Files = e.XPathSelectElement("./value/array/data")
.Elements()
.Select(x => new Aria2File(x))
.ToArray();
}
else if (name == "gid")
{
Gid = e.Element("value").GetStringValue();
}
else if (name == "status")
{
Status = e.Element("value").GetStringValue();
}
else if (name == "totalLength")
{
TotalLength = e.Element("value").GetStringValue();
}
else if (name == "uploadLength")
{
UploadLength = e.Element("value").GetStringValue();
}
else if (name == "errorMessage")
{
ErrorMessage = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("bitfield")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Bitfield;
public Aria2Bittorrent Bittorrent { get; set; }
public string InfoHash { get; set; }
public string CompletedLength { get; set; }
public string DownloadSpeed { get; set; }
public Aria2File[] Files { get; set; }
public string Gid { get; set; }
public string Status { get; set; }
public string TotalLength { get; set; }
public string UploadLength { get; set; }
public string ErrorMessage { get; set; }
[XmlRpcMember("infoHash")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string InfoHash;
[XmlRpcMember("completedLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string CompletedLength;
[XmlRpcMember("connections")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Connections;
[XmlRpcMember("dir")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Dir;
[XmlRpcMember("downloadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string DownloadSpeed;
[XmlRpcMember("files")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public Aria2File[] Files;
[XmlRpcMember("gid")]
public string Gid;
[XmlRpcMember("numPieces")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string NumPieces;
[XmlRpcMember("pieceLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string PieceLength;
[XmlRpcMember("status")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Status;
[XmlRpcMember("totalLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string TotalLength;
[XmlRpcMember("uploadLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadLength;
[XmlRpcMember("uploadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadSpeed;
[XmlRpcMember("errorMessage")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string ErrorMessage;
}
}

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
using System.Net;
using CookComputing.XmlRpc;
using NLog;
namespace NzbDrone.Core.Download.Clients.Aria2
{
@@ -12,103 +12,46 @@ namespace NzbDrone.Core.Download.Clients.Aria2
string GetVersion(Aria2Settings settings);
string AddUri(Aria2Settings settings, string magnet);
string AddTorrent(Aria2Settings settings, byte[] torrent);
Dictionary<string, string> GetGlobals(Aria2Settings settings);
List<Aria2Status> GetTorrents(Aria2Settings settings);
Aria2Status GetFromGID(Aria2Settings settings, string gid);
}
public interface IAria2 : IXmlRpcProxy
{
[XmlRpcMethod("aria2.getVersion")]
Aria2Version GetVersion(string token);
[XmlRpcMethod("aria2.addUri")]
string AddUri(string token, string[] uri);
[XmlRpcMethod("aria2.addTorrent")]
string AddTorrent(string token, byte[] torrent);
[XmlRpcMethod("aria2.forceRemove")]
string Remove(string token, string gid);
[XmlRpcMethod("aria2.tellStatus")]
Aria2Status GetFromGid(string token, string gid);
[XmlRpcMethod("aria2.getGlobalOption")]
XmlRpcStruct GetGlobalOption(string token);
[XmlRpcMethod("aria2.tellActive")]
Aria2Status[] GetActives(string token);
[XmlRpcMethod("aria2.tellWaiting")]
Aria2Status[] GetWaitings(string token, int offset, int num);
[XmlRpcMethod("aria2.tellStopped")]
Aria2Status[] GetStoppeds(string token, int offset, int num);
}
public class Aria2Proxy : IAria2Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public Aria2Proxy(IHttpClient httpClient)
public Aria2Proxy(Logger logger)
{
_httpClient = httpClient;
}
public string GetVersion(Aria2Settings settings)
{
var response = ExecuteRequest(settings, "aria2.getVersion", GetToken(settings));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
var version = new Aria2Version(element);
return version.Version;
}
public Aria2Status GetFromGID(Aria2Settings settings, string gid)
{
var response = ExecuteRequest(settings, "aria2.tellStatus", GetToken(settings), gid);
var element = response.XPathSelectElement("./methodResponse/params/param/value");
return new Aria2Status(element);
}
private List<Aria2Status> GetTorrentsMethod(Aria2Settings settings, string method, params object[] args)
{
var allArgs = new List<object> { GetToken(settings) };
if (args.Any())
{
allArgs.AddRange(args);
}
var response = ExecuteRequest(settings, method, allArgs.ToArray());
var element = response.XPathSelectElement("./methodResponse/params/param/value/array/data");
var torrents = element?.Elements()
.Select(x => new Aria2Status(x))
.ToList()
?? new List<Aria2Status>();
return torrents;
}
public List<Aria2Status> GetTorrents(Aria2Settings settings)
{
var active = GetTorrentsMethod(settings, "aria2.tellActive");
var waiting = GetTorrentsMethod(settings, "aria2.tellWaiting", 0, 10 * 1024);
var stopped = GetTorrentsMethod(settings, "aria2.tellStopped", 0, 10 * 1024);
var items = new List<Aria2Status>();
items.AddRange(active);
items.AddRange(waiting);
items.AddRange(stopped);
return items;
}
public Dictionary<string, string> GetGlobals(Aria2Settings settings)
{
var response = ExecuteRequest(settings, "aria2.getGlobalOption", GetToken(settings));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
var result = new Aria2Dict(element);
return result.Dict;
}
public string AddUri(Aria2Settings settings, string magnet)
{
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet });
var gid = response.GetStringResponse();
return gid;
}
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent);
var gid = response.GetStringResponse();
return gid;
_logger = logger;
}
private string GetToken(Aria2Settings settings)
@@ -116,29 +59,86 @@ namespace NzbDrone.Core.Download.Clients.Aria2
return $"token:{settings?.SecretToken}";
}
private XDocument ExecuteRequest(Aria2Settings settings, string methodName, params object[] args)
private string GetURL(Aria2Settings settings)
{
var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.RpcPath)
return $"http{(settings.UseSsl ? "s" : "")}://{settings.Host}:{settings.Port}{settings.RpcPath}";
}
public string GetVersion(Aria2Settings settings)
{
_logger.Debug("> aria2.getVersion");
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion(GetToken(settings)));
_logger.Debug("< aria2.getVersion");
return version.Version;
}
public Aria2Status GetFromGID(Aria2Settings settings, string gid)
{
_logger.Debug("> aria2.tellStatus");
var client = BuildClient(settings);
var found = ExecuteRequest(() => client.GetFromGid(GetToken(settings), gid));
_logger.Debug("< aria2.tellStatus");
return found;
}
public string AddUri(Aria2Settings settings, string magnet)
{
_logger.Debug("> aria2.addUri");
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddUri(GetToken(settings), new[] { magnet }));
_logger.Debug("< aria2.addUri");
return gid;
}
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
_logger.Debug("> aria2.addTorrent");
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddTorrent(GetToken(settings), torrent));
_logger.Debug("< aria2.addTorrent");
return gid;
}
private IAria2 BuildClient(Aria2Settings settings)
{
var client = XmlRpcProxyGen.Create<IAria2>();
client.Url = GetURL(settings);
return client;
}
private T ExecuteRequest<T>(Func<T> task)
{
try
{
LogResponseContent = true,
};
var request = requestBuilder.Call(methodName, args).Build();
var response = _httpClient.Execute(request);
var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
{
var fault = new Aria2Fault(faultElement);
throw new DownloadClientException($"Aria2 returned error code {fault.FaultCode}: {fault.FaultString}");
return task();
}
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to aria2, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to aria2, certificate validation failed.", ex);
}
return doc;
throw new DownloadClientUnavailableException("Unable to connect to aria2, please check your settings", ex);
}
}
}
}

View File

@@ -1,28 +0,0 @@
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
public class RTorrentFault
{
public RTorrentFault(XElement element)
{
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").GetIntValue();
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
public int FaultCode { get; set; }
public string FaultString { get; set; }
}
}

View File

@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Xml.Linq;
using System.Xml.XPath;
using CookComputing.XmlRpc;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
@@ -20,70 +18,122 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void RemoveTorrent(string hash, RTorrentSettings settings);
void SetTorrentLabel(string hash, string label, RTorrentSettings settings);
bool HasHashTorrent(string hash, RTorrentSettings settings);
void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
}
public interface IRTorrent : IXmlRpcProxy
{
[XmlRpcMethod("d.multicall2")]
object[] TorrentMulticall(params string[] parameters);
[XmlRpcMethod("load.normal")]
int LoadNormal(string target, string data, params string[] commands);
[XmlRpcMethod("load.start")]
int LoadStart(string target, string data, params string[] commands);
[XmlRpcMethod("load.raw")]
int LoadRaw(string target, byte[] data, params string[] commands);
[XmlRpcMethod("load.raw_start")]
int LoadRawStart(string target, byte[] data, params string[] commands);
[XmlRpcMethod("d.erase")]
int Remove(string hash);
[XmlRpcMethod("d.name")]
string GetName(string hash);
[XmlRpcMethod("d.custom1.set")]
string SetLabel(string hash, string label);
[XmlRpcMethod("system.client_version")]
string GetVersion();
}
public class RTorrentProxy : IRTorrentProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public RTorrentProxy(IHttpClient httpClient)
public RTorrentProxy(Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public string GetVersion(RTorrentSettings settings)
{
var document = ExecuteRequest(settings, "system.client_version");
_logger.Debug("Executing remote method: system.client_version");
return document.Descendants("string").FirstOrDefault()?.Value ?? "0.0.0";
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion());
return version;
}
public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings)
{
var document = ExecuteRequest(settings,
"d.multicall2",
"",
"",
"d.name=", // string
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=", //long
"d.timestamp.finished="); // long (unix timestamp)
_logger.Debug("Executing remote method: d.multicall2");
var torrents = document.XPathSelectElement("./methodResponse/params/param/value/array/data")
?.Elements()
.Select(x => new RTorrentTorrent(x))
.ToList()
?? new List<RTorrentTorrent>();
var client = BuildClient(settings);
var ret = ExecuteRequest(() => client.TorrentMulticall(
"",
"",
"d.name=", // string
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=")); //long
return torrents;
_logger.Trace(ret.ToJson());
var items = new List<RTorrentTorrent>();
foreach (object[] torrent in ret)
{
var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]);
var item = new RTorrentTorrent();
item.Name = (string)torrent[0];
item.Hash = (string)torrent[1];
item.Path = (string)torrent[2];
item.Category = labelDecoded;
item.TotalSize = (long)torrent[4];
item.RemainingSize = (long)torrent[5];
item.DownRate = (long)torrent[6];
item.Ratio = (long)torrent[7];
item.IsOpen = Convert.ToBoolean((long)torrent[8]);
item.IsActive = Convert.ToBoolean((long)torrent[9]);
item.IsFinished = Convert.ToBoolean((long)torrent[10]);
items.Add(item);
}
return items;
}
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{
var args = new List<object> { "", torrentUrl };
args.AddRange(GetCommands(label, priority, directory));
XDocument response;
if (settings.AddStopped)
var client = BuildClient(settings);
var response = ExecuteRequest(() =>
{
response = ExecuteRequest(settings, "load.normal", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.start", args.ToArray());
}
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.normal");
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.start");
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
}
});
if (response.GetIntResponse() != 0)
if (response != 0)
{
throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
}
@@ -91,21 +141,22 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{
var args = new List<object> { "", fileContent };
args.AddRange(GetCommands(label, priority, directory));
XDocument response;
if (settings.AddStopped)
var client = BuildClient(settings);
var response = ExecuteRequest(() =>
{
response = ExecuteRequest(settings, "load.raw", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.raw_start", args.ToArray());
}
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.raw");
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.raw_start");
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
}
});
if (response.GetIntResponse() != 0)
if (response != 0)
{
throw new DownloadClientException("Could not add torrent: {0}.", fileName);
}
@@ -113,29 +164,25 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
{
var response = ExecuteRequest(settings, "d.custom1.set", hash, label);
_logger.Debug("Executing remote method: d.custom1.set");
if (response.GetStringResponse() != label)
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.SetLabel(hash, label));
if (response != label)
{
throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label);
}
}
public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings)
{
var response = ExecuteRequest(settings, "d.views.push_back_unique", hash, view);
if (response.GetIntResponse() != 0)
{
throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash);
}
}
public void RemoveTorrent(string hash, RTorrentSettings settings)
{
var response = ExecuteRequest(settings, "d.erase", hash);
_logger.Debug("Executing remote method: d.erase");
if (response.GetIntResponse() != 0)
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.Remove(hash));
if (response != 0)
{
throw new DownloadClientException("Could not remove torrent: {0}.", hash);
}
@@ -143,10 +190,13 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public bool HasHashTorrent(string hash, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try
{
var response = ExecuteRequest(settings, "d.name", hash);
var name = response.GetStringResponse();
var name = ExecuteRequest(() => client.GetName(hash));
if (name.IsNullOrWhiteSpace())
{
@@ -185,34 +235,45 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return result.ToArray();
}
private XDocument ExecuteRequest(RTorrentSettings settings, string methodName, params object[] args)
private IRTorrent BuildClient(RTorrentSettings settings)
{
var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
LogResponseContent = true,
};
var client = XmlRpcProxyGen.Create<IRTorrent>();
client.Url = string.Format(@"{0}://{1}:{2}/{3}",
settings.UseSsl ? "https" : "http",
settings.Host,
settings.Port,
settings.UrlBase);
client.EnableCompression = true;
if (!settings.Username.IsNullOrWhiteSpace())
{
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
}
var request = requestBuilder.Call(methodName, args).Build();
return client;
}
var response = _httpClient.Execute(request);
var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
private T ExecuteRequest<T>(Func<T> task)
{
try
{
var fault = new RTorrentFault(faultElement);
throw new DownloadClientException($"rTorrent returned error code {fault.FaultCode}: {fault.FaultString}");
return task();
}
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, certificate validation failed.", ex);
}
return doc;
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex);
}
}
}
}

View File

@@ -1,35 +1,7 @@
using System;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
namespace NzbDrone.Core.Download.Clients.RTorrent
{
public class RTorrentTorrent
{
public RTorrentTorrent()
{
}
public RTorrentTorrent(XElement element)
{
var data = element.Descendants("value").ToList();
Name = data[0].GetStringValue();
Hash = data[1].GetStringValue();
Path = data[2].GetStringValue();
Category = HttpUtility.UrlDecode(data[3].GetStringValue());
TotalSize = data[4].GetLongValue();
RemainingSize = data[5].GetLongValue();
DownRate = data[6].GetLongValue();
Ratio = data[7].GetLongValue();
IsOpen = Convert.ToBoolean(data[8].GetLongValue());
IsActive = Convert.ToBoolean(data[9].GetLongValue());
IsFinished = Convert.ToBoolean(data[10].GetLongValue());
FinishedTime = data[11].GetLongValue();
}
public string Name { get; set; }
public string Hash { get; set; }
public string Path { get; set; }
@@ -38,7 +10,6 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public long RemainingSize { get; set; }
public long DownRate { get; set; }
public long Ratio { get; set; }
public long FinishedTime { get; set; }
public bool IsFinished { get; set; }
public bool IsOpen { get; set; }
public bool IsActive { get; set; }

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