mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-18 16:24:34 -04:00
Compare commits
1 Commits
slack-refa
...
ending-col
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da5feda71b |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Bug Report
|
||||
title: "[BUG]: "
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
@@ -63,11 +64,12 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Trace Logs?
|
||||
label: Anything else?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Feature Request
|
||||
title: "[FEAT]: "
|
||||
description: 'Suggest an idea for Radarr'
|
||||
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||
body:
|
||||
|
||||
45
README.md
45
README.md
@@ -1,21 +1,19 @@
|
||||
# Radarr
|
||||
|
||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/radarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
[](#mega-sponsors)
|
||||
|
||||
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
|
||||
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
|
||||
|
||||
## Major Features Include
|
||||
## Major Features Include:
|
||||
|
||||
* Adding new movies with lots of information, such as trailers, ratings, etc.
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
|
||||
* Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
||||
* Automatic failed download handling will try another release if one fails
|
||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
* Full integration with SABnzbd and NZBGet
|
||||
@@ -23,60 +21,53 @@ Note that only one type of a given movie is supported. If you want both an 4k ve
|
||||
* Automatically importing downloaded movies
|
||||
* Recognizing Special Editions, Director's Cut, etc.
|
||||
* Identifying releases with hardcoded subs
|
||||
* Identifying releases with AKA movie names
|
||||
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
|
||||
* Full integration with Kodi and Plex (notifications, library updates)
|
||||
* QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported
|
||||
* Full integration with Kodi, Plex (notification, library update)
|
||||
* A beautiful UI
|
||||
* Importing Metadata such as trailers or subtitles
|
||||
* Adding metadata such as posters and information for Kodi and others to use
|
||||
* Advanced customization for profiles, such that Radarr will always download the copy you want
|
||||
* A beautiful UI
|
||||
|
||||
## Support
|
||||
|
||||
[](https://wiki.servarr.com/radarr)
|
||||
[](https://radarr.video/discord)
|
||||
[](https://www.reddit.com/r/Radarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://radarr.video/discord)
|
||||
[](https://www.reddit.com/r/Radarr)
|
||||
[](https://github.com/Radarr/Radarr/issues)
|
||||
[](https://wiki.servarr.com/radarr)
|
||||
|
||||
## Contributors & Developers
|
||||
|
||||
[API Documentation](https://radarr.video/docs/api/)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
- [Contribute (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/radarr/contributing)
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
<a href="https://github.com/Radarr/Radarr/graphs/contributors"><img src="https://opencollective.com/Radarr/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
[](https://github.com/Radarr/Radarr/graphs/contributors)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Radarr#backer)
|
||||
|
||||
[](https://opencollective.com/Radarr#backer)
|
||||
<img src="https://opencollective.com/Radarr/backers.svg?width=890"></a>
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Radarr#sponsor)
|
||||
|
||||
[](https://opencollective.com/Radarr#sponsor)
|
||||
<img src="https://opencollective.com/Radarr/sponsors.svg?width=890"></a>
|
||||
|
||||
## Mega Sponsors
|
||||
|
||||
[](https://opencollective.com/Radarr#mega-sponsor)
|
||||
<img src="https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890"></a>
|
||||
|
||||
## JetBrains
|
||||
|
||||
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||
|
||||
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2022
|
||||
* Copyright 2010-2021
|
||||
|
||||
@@ -13,7 +13,7 @@ variables:
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.100'
|
||||
dotnetVersion: '5.0.401'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
|
||||
trigger:
|
||||
@@ -67,7 +67,7 @@ stages:
|
||||
enableAnalysis: 'true'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: 'macos-10.14'
|
||||
enableAnalysis: 'false'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
@@ -111,23 +111,23 @@ stages:
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
- publish: '$(testsFolder)/net5.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
@@ -144,7 +144,7 @@ stages:
|
||||
imageName: 'ubuntu-18.04'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: 'macos-10.14'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
@@ -203,12 +203,12 @@ stages:
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x86
|
||||
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x86
|
||||
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x64
|
||||
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x64
|
||||
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
@@ -241,7 +241,6 @@ stages:
|
||||
- bash: ./build.sh --packages --enable-bsd
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
find . -name "ffprobe" -exec chmod a+x {} \;
|
||||
find . -name "Radarr" -exec chmod a+x {} \;
|
||||
find . -name "Radarr.Update" -exec chmod a+x {} \;
|
||||
displayName: Set executable bits
|
||||
@@ -251,44 +250,29 @@ stages:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows x86 Core zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS x64 Core app
|
||||
displayName: Create MacOS Core app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS x64 Core tar
|
||||
displayName: Create MacOS Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
inputs:
|
||||
@@ -296,7 +280,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
inputs:
|
||||
@@ -304,7 +288,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
inputs:
|
||||
@@ -312,15 +296,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Musl Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Core tar
|
||||
inputs:
|
||||
@@ -328,7 +304,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
inputs:
|
||||
@@ -336,7 +312,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
inputs:
|
||||
@@ -344,7 +320,7 @@ stages:
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Packages'
|
||||
displayName: Publish Packages
|
||||
@@ -407,7 +383,7 @@ stages:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: 'macos-10.14'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
@@ -441,13 +417,16 @@ stages:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
targetPath: $(testsFolder)
|
||||
- bash: |
|
||||
wget https://mediaarea.net/repo/deb/repo-mediaarea_1.0-11_all.deb
|
||||
sudo dpkg -i repo-mediaarea_1.0-11_all.deb
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-unauthenticated libmediainfo-dev libmediainfo0v5 mediainfo
|
||||
displayName: Install mediainfo
|
||||
condition: and(succeeded(), eq(variables['testName'], 'LinuxCore'))
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
chmod a+x _tests/ffprobe
|
||||
displayName: Make ffprobe Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
@@ -495,9 +474,6 @@ stages:
|
||||
buildType: 'current'
|
||||
artifactName: $(artifactName)
|
||||
targetPath: $(testsFolder)
|
||||
- bash: |
|
||||
chmod a+x _tests/ffprobe
|
||||
displayName: Make ffprobe Executable
|
||||
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
@@ -541,7 +517,7 @@ stages:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: 'macos-10.14'
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
@@ -716,7 +692,7 @@ stages:
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: 'macos-10.14'
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Windows:
|
||||
@@ -889,8 +865,8 @@ stages:
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
./build.sh --backend -f net5.0 -r win-x64
|
||||
TEST_DIR=_tests/net5.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
|
||||
54
build.sh
54
build.sh
@@ -129,7 +129,7 @@ PackageLinux()
|
||||
|
||||
echo "Adding Radarr.Mono to UpdatePackage"
|
||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||
fi
|
||||
@@ -140,13 +140,12 @@ PackageLinux()
|
||||
PackageMacOS()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating MacOS Package for $framework $runtime"
|
||||
ProgressStart "Creating MacOS Package for $framework"
|
||||
|
||||
local folder=$artifactsFolder/$runtime/$framework/Radarr
|
||||
local folder=$artifactsFolder/macos/$framework/Radarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
PackageFiles "$folder" "$framework" "osx-x64"
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
@@ -157,7 +156,7 @@ PackageMacOS()
|
||||
|
||||
echo "Adding Radarr.Mono to UpdatePackage"
|
||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||
fi
|
||||
@@ -168,11 +167,10 @@ PackageMacOS()
|
||||
PackageMacOSApp()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating macOS App Package for $framework $runtime"
|
||||
ProgressStart "Creating macOS App Package for $framework"
|
||||
|
||||
local folder="$artifactsFolder/$runtime-app/$framework"
|
||||
local folder=$artifactsFolder/macos-app/$framework
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
@@ -180,7 +178,7 @@ PackageMacOSApp()
|
||||
mkdir -p $folder/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $artifactsFolder/$runtime/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
|
||||
cp -r $artifactsFolder/macos/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update
|
||||
@@ -227,8 +225,8 @@ Package()
|
||||
PackageWindows "$framework" "$runtime"
|
||||
;;
|
||||
osx)
|
||||
PackageMacOS "$framework" "$runtime"
|
||||
PackageMacOSApp "$framework" "$runtime"
|
||||
PackageMacOS "$framework"
|
||||
PackageMacOSApp "$framework"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -328,14 +326,14 @@ then
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "net6.0" "win-x64"
|
||||
PackageTests "net6.0" "win-x86"
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
PackageTests "net5.0" "win-x64"
|
||||
PackageTests "net5.0" "win-x86"
|
||||
PackageTests "net5.0" "linux-x64"
|
||||
PackageTests "net5.0" "linux-musl-x64"
|
||||
PackageTests "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
then
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net5.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -364,19 +362,17 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "net6.0" "win-x64"
|
||||
Package "net6.0" "win-x86"
|
||||
Package "net6.0" "linux-x64"
|
||||
Package "net6.0" "linux-musl-x64"
|
||||
Package "net6.0" "linux-arm64"
|
||||
Package "net6.0" "linux-musl-arm64"
|
||||
Package "net6.0" "linux-arm"
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
Package "net5.0" "win-x64"
|
||||
Package "net5.0" "win-x86"
|
||||
Package "net5.0" "linux-x64"
|
||||
Package "net5.0" "linux-musl-x64"
|
||||
Package "net5.0" "linux-arm64"
|
||||
Package "net5.0" "linux-musl-arm64"
|
||||
Package "net5.0" "linux-arm"
|
||||
Package "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
then
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net5.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
|
||||
@@ -282,17 +282,6 @@ class Queue extends Component {
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
@@ -332,7 +332,6 @@ class QueueRow extends Component {
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
|
||||
@@ -66,8 +66,7 @@ class RemoveQueueItemModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist } = this.state;
|
||||
@@ -90,22 +89,18 @@ class RemoveQueueItemModal extends Component {
|
||||
{translate('RemoveFromQueueText', [sourceTitle])}
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||
@@ -142,7 +137,6 @@ RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -67,8 +67,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist } = this.state;
|
||||
@@ -83,34 +82,30 @@ class RemoveQueueItemsModal extends Component {
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
Remove Selected Item{selectedCount > 1 ? 's' : ''}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
|
||||
{translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', [selectedCount, selectedCount > 1 ? 's' : ''])}
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
Blocklist Release{selectedCount > 1 ? 's' : ''}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -146,7 +141,6 @@ RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -16,9 +16,4 @@
|
||||
color: #3a3f51;
|
||||
font-size: 21px;
|
||||
line-height: inherit;
|
||||
|
||||
&.small {
|
||||
color: #909293;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import styles from './FieldSet.css';
|
||||
|
||||
class FieldSet extends Component {
|
||||
@@ -11,14 +9,13 @@ class FieldSet extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
size,
|
||||
legend,
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<fieldset className={styles.fieldSet}>
|
||||
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
|
||||
<legend className={styles.legend}>
|
||||
{legend}
|
||||
</legend>
|
||||
{children}
|
||||
@@ -29,13 +26,8 @@ class FieldSet extends Component {
|
||||
}
|
||||
|
||||
FieldSet.propTypes = {
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
FieldSet.defaultProps = {
|
||||
size: sizes.MEDIUM
|
||||
};
|
||||
|
||||
export default FieldSet;
|
||||
|
||||
@@ -64,7 +64,6 @@ function ProviderFieldFormGroup(props) {
|
||||
label,
|
||||
helpText,
|
||||
helpLink,
|
||||
placeholder,
|
||||
value,
|
||||
type,
|
||||
advanced,
|
||||
@@ -97,7 +96,6 @@ function ProviderFieldFormGroup(props) {
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
helpLink={helpLink}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
values={getSelectValues(selectOptions)}
|
||||
errors={errors}
|
||||
@@ -123,7 +121,6 @@ ProviderFieldFormGroup.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
helpText: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
type: PropTypes.string.isRequired,
|
||||
advanced: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -151,8 +151,7 @@
|
||||
.qualityProfileName,
|
||||
.statusName,
|
||||
.studio,
|
||||
.collection,
|
||||
.genres {
|
||||
.collection {
|
||||
font-weight: 300;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
@@ -266,7 +266,6 @@ class MovieDetails extends Component {
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
studio,
|
||||
genres,
|
||||
collection,
|
||||
overview,
|
||||
youTubeTrailerId,
|
||||
@@ -583,19 +582,6 @@ class MovieDetails extends Component {
|
||||
</span>
|
||||
</InfoLabel>
|
||||
}
|
||||
|
||||
{
|
||||
!!genres.length && !isSmallScreen &&
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('Genres')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.genres}>
|
||||
{genres.join(', ')}
|
||||
</span>
|
||||
</InfoLabel>
|
||||
}
|
||||
</div>
|
||||
|
||||
<Measure onMeasure={this.onMeasure}>
|
||||
@@ -780,7 +766,6 @@ MovieDetails.propTypes = {
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
studio: PropTypes.string,
|
||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
collection: PropTypes.object,
|
||||
youTubeTrailerId: PropTypes.string,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
@@ -813,7 +798,6 @@ MovieDetails.propTypes = {
|
||||
};
|
||||
|
||||
MovieDetails.defaultProps = {
|
||||
genres: [],
|
||||
tags: [],
|
||||
isSaving: false,
|
||||
sizeOnDisk: 0
|
||||
|
||||
@@ -33,9 +33,3 @@
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ class MovieFileEditorRow extends Component {
|
||||
mediaInfo,
|
||||
relativePath,
|
||||
size,
|
||||
releaseGroup,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
@@ -156,12 +155,6 @@ class MovieFileEditorRow extends Component {
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.releaseGroup}
|
||||
>
|
||||
{releaseGroup}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.formats}
|
||||
>
|
||||
@@ -223,7 +216,6 @@ MovieFileEditorRow.propTypes = {
|
||||
size: PropTypes.number.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
releaseGroup: PropTypes.string,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -39,11 +39,6 @@ const columns = [
|
||||
label: translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: translate('ReleaseGroup'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality.customFormats',
|
||||
label: translate('Formats'),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -14,7 +13,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
@@ -46,10 +45,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
implementationName,
|
||||
name,
|
||||
enable,
|
||||
protocol,
|
||||
priority,
|
||||
removeCompletedDownloads,
|
||||
removeFailedDownloads,
|
||||
fields,
|
||||
message
|
||||
} = item;
|
||||
@@ -140,38 +136,6 @@ class EditDownloadClientModalContent extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FieldSet
|
||||
size={sizes.SMALL}
|
||||
legend={translate('CompletedDownloadHandling')}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveCompleted')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeCompletedDownloads"
|
||||
helpText={translate('RemoveCompletedDownloadsHelpText')}
|
||||
{...removeCompletedDownloads}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
protocol.value !== 'torrent' &&
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFailedDownloads"
|
||||
helpText={translate('RemoveFailedDownloadsHelpText')}
|
||||
{...removeFailedDownloads}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</FieldSet>
|
||||
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import { inputTypes, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function DownloadClientOptions(props) {
|
||||
@@ -35,15 +34,11 @@ function DownloadClientOptions(props) {
|
||||
}
|
||||
|
||||
{
|
||||
hasSettings && !isFetching && !error && advancedSettings &&
|
||||
hasSettings && !isFetching && !error &&
|
||||
<div>
|
||||
<FieldSet legend={translate('CompletedDownloadHandling')}>
|
||||
<Form>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -55,6 +50,22 @@ function DownloadClientOptions(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Remove')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeCompletedDownloads"
|
||||
helpText={translate('RemoveCompletedDownloadsHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.removeCompletedDownloads}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
@@ -91,10 +102,23 @@ function DownloadClientOptions(props) {
|
||||
{...settings.autoRedownloadFailed}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Remove')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFailedDownloads"
|
||||
helpText={translate('RemoveFailedDownloadsHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.removeFailedDownloads}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('RemoveDownloadsAlert')}
|
||||
</Alert>
|
||||
</FieldSet>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ function createRemoveItemHandler(section, url) {
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/${id}?${$.param(queryParams, true)}`,
|
||||
dataType: 'text',
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
|
||||
@@ -600,7 +600,6 @@ export const actionHandlers = handleThunks({
|
||||
const promise = createAjaxRequest({
|
||||
url: '/exclusions/bulk',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(exclusions)
|
||||
}).request;
|
||||
|
||||
|
||||
10
package.json
10
package.json
@@ -30,7 +30,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@microsoft/signalr": "6.0.0",
|
||||
"@microsoft/signalr": "5.0.10",
|
||||
"@sentry/browser": "6.13.2",
|
||||
"@sentry/integrations": "6.13.2",
|
||||
"classnames": "2.3.1",
|
||||
@@ -82,7 +82,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.13.16",
|
||||
"@babel/eslint-parser": "7.13.14",
|
||||
"@babel/plugin-proposal-class-properties": "7.13.0",
|
||||
"@babel/plugin-proposal-decorators": "7.13.15",
|
||||
"@babel/plugin-proposal-export-default-from": "7.12.13",
|
||||
@@ -95,6 +94,7 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.13.15",
|
||||
"@babel/preset-react": "7.13.13",
|
||||
"@babel/eslint-parser": "7.13.14",
|
||||
"autoprefixer": "10.2.5",
|
||||
"babel-loader": "8.2.2",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
@@ -125,12 +125,12 @@
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "2.0.0",
|
||||
"stylelint": "13.13.0",
|
||||
"stylelint-order": "4.1.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.35.1",
|
||||
"webpack-cli": "4.6.0",
|
||||
"webpack-livereload-plugin": "3.0.1",
|
||||
"worker-loader": "3.0.8"
|
||||
"worker-loader": "3.0.8",
|
||||
"stylelint": "13.13.0",
|
||||
"stylelint-order": "4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
<!-- Common to all Radarr Projects -->
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;osx-arm64;linux-x64;linux-musl-x64;linux-arm;linux-musl-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;linux-x64;linux-musl-x64;linux-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
|
||||
|
||||
<RadarrRootDir>$(MSBuildThisFileDirectory)..\</RadarrRootDir>
|
||||
|
||||
@@ -90,13 +89,13 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="2.1.62" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
|
||||
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net5.0'">
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/fluentmigrator/fluentmigrator/_packaging/fluentmigrator/nuget/v3/index.json" />
|
||||
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
||||
<add key="FFMpegCore" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FFMpegCore/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -16,11 +15,8 @@ using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Http.Dispatchers;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Security;
|
||||
using NzbDrone.Test.Common;
|
||||
using NzbDrone.Test.Common.Categories;
|
||||
using HttpClient = NzbDrone.Common.Http.HttpClient;
|
||||
|
||||
namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
@@ -35,8 +31,6 @@ namespace NzbDrone.Common.Test.Http
|
||||
private string _httpBinHost;
|
||||
private string _httpBinHost2;
|
||||
|
||||
private System.Net.Http.HttpClient _httpClient = new ();
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void FixtureSetUp()
|
||||
{
|
||||
@@ -44,7 +38,7 @@ namespace NzbDrone.Common.Test.Http
|
||||
var mainHost = "httpbin.servarr.com";
|
||||
|
||||
// Use mirrors for tests that use two hosts
|
||||
var candidates = new[] { "httpbin1.servarr.com" };
|
||||
var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" };
|
||||
|
||||
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
|
||||
_httpBinHost = mainHost;
|
||||
@@ -52,20 +46,29 @@ namespace NzbDrone.Common.Test.Http
|
||||
|
||||
TestLogger.Info($"{candidates.Length} TestSites available.");
|
||||
|
||||
_httpBinSleep = 10;
|
||||
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10;
|
||||
}
|
||||
|
||||
private bool IsTestSiteAvailable(string site)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
|
||||
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest;
|
||||
var res = req.GetResponse() as HttpWebResponse;
|
||||
if (res.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
|
||||
try
|
||||
{
|
||||
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
|
||||
res = req.GetResponse() as HttpWebResponse;
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
res = ex.Response as HttpWebResponse;
|
||||
}
|
||||
|
||||
if (res == null || res.StatusCode != (HttpStatusCode)429)
|
||||
{
|
||||
@@ -92,13 +95,10 @@ namespace NzbDrone.Common.Test.Http
|
||||
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
|
||||
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
|
||||
|
||||
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
|
||||
|
||||
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
|
||||
|
||||
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
|
||||
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
|
||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
|
||||
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
|
||||
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
|
||||
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
|
||||
@@ -138,28 +138,6 @@ namespace NzbDrone.Common.Test.Http
|
||||
response.Content.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[TestCase(CertificateValidationType.Enabled)]
|
||||
[TestCase(CertificateValidationType.DisabledForLocalAddresses)]
|
||||
public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
|
||||
var request = new HttpRequest($"https://expired.badssl.com");
|
||||
|
||||
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
|
||||
ExceptionVerification.ExpectedErrors(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void bad_ssl_should_pass_if_remote_validation_disabled()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
|
||||
|
||||
var request = new HttpRequest($"https://expired.badssl.com");
|
||||
|
||||
Subject.Execute(request);
|
||||
ExceptionVerification.ExpectedErrors(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_execute_typed_get()
|
||||
{
|
||||
@@ -184,44 +162,15 @@ namespace NzbDrone.Common.Test.Http
|
||||
response.Resource.Data.Should().Be(message);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_execute_post_with_content_type()
|
||||
[TestCase("gzip")]
|
||||
public void should_execute_get_using_gzip(string compression)
|
||||
{
|
||||
var message = "{ my: 1 }";
|
||||
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/post");
|
||||
request.SetContent(message);
|
||||
request.Headers.ContentType = "application/json";
|
||||
|
||||
var response = Subject.Post<HttpBinResource>(request);
|
||||
|
||||
response.Resource.Data.Should().Be(message);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_execute_get_using_gzip()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/{compression}");
|
||||
|
||||
var response = Subject.Get<HttpBinResource>(request);
|
||||
|
||||
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
|
||||
|
||||
response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression);
|
||||
response.Resource.Gzipped.Should().BeTrue();
|
||||
response.Resource.Brotli.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_execute_get_using_brotli()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
||||
|
||||
var response = Subject.Get<HttpBinResource>(request);
|
||||
|
||||
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
|
||||
|
||||
response.Resource.Gzipped.Should().BeFalse();
|
||||
response.Resource.Brotli.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase(HttpStatusCode.Unauthorized)]
|
||||
@@ -388,38 +337,13 @@ namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
var file = GetTempFilePath();
|
||||
|
||||
Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
|
||||
Assert.Throws<WebException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
|
||||
|
||||
File.Exists(file).Should().BeFalse();
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_write_redirect_content_to_stream()
|
||||
{
|
||||
var file = GetTempFilePath();
|
||||
|
||||
using (var fileStream = new FileStream(file, FileMode.Create))
|
||||
{
|
||||
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
|
||||
request.AllowAutoRedirect = false;
|
||||
request.ResponseStream = fileStream;
|
||||
|
||||
var response = Subject.Get(request);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Moved);
|
||||
}
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
|
||||
File.Exists(file).Should().BeTrue();
|
||||
|
||||
var fileInfo = new FileInfo(file);
|
||||
|
||||
fileInfo.Length.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_send_cookie()
|
||||
{
|
||||
@@ -819,7 +743,7 @@ namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
|
||||
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}";
|
||||
|
||||
var requestSet = new HttpRequest(url);
|
||||
requestSet.AllowAutoRedirect = false;
|
||||
@@ -849,7 +773,6 @@ namespace NzbDrone.Common.Test.Http
|
||||
public string Url { get; set; }
|
||||
public string Data { get; set; }
|
||||
public bool Gzipped { get; set; }
|
||||
public bool Brotli { get; set; }
|
||||
}
|
||||
|
||||
public class HttpCookieResource
|
||||
|
||||
@@ -62,7 +62,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase("Hardlinking episode file: /home/mySecret/Downloads to /media/abc.mkv")]
|
||||
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
|
||||
// Announce URLs (passkeys) Magnet & Tracker
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Host\Radarr.Host.csproj" />
|
||||
|
||||
@@ -3,8 +3,6 @@ using DryIoc;
|
||||
using DryIoc.Microsoft.DependencyInjection;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -27,15 +25,12 @@ namespace NzbDrone.Common.Test
|
||||
.AddNzbDroneLogger()
|
||||
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
||||
.AddDummyDatabase()
|
||||
.AddStartupContext(new StartupContext("first", "second"));
|
||||
.AddStartupContext(new StartupContext("first", "second"))
|
||||
.GetServiceProvider();
|
||||
|
||||
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||
container.GetRequiredService<IAppFolderFactory>().Register();
|
||||
|
||||
var serviceProvider = container.GetServiceProvider();
|
||||
|
||||
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
||||
|
||||
Mocker.SetConstant<System.IServiceProvider>(serviceProvider);
|
||||
Mocker.SetConstant<System.IServiceProvider>(container);
|
||||
|
||||
var handlers = Subject.BuildAll<IHandle<ApplicationStartedEvent>>()
|
||||
.Select(c => c.GetType().FullName);
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Common
|
||||
Stream inStream = File.OpenRead(compressedFile);
|
||||
Stream gzipStream = new GZipInputStream(inStream);
|
||||
|
||||
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null);
|
||||
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream);
|
||||
tarArchive.ExtractContents(destination);
|
||||
tarArchive.Close();
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ namespace NzbDrone.Common.Composition
|
||||
{
|
||||
mappedName = "libsqlite3.so.0";
|
||||
}
|
||||
else if (libraryName == "mediainfo")
|
||||
{
|
||||
mappedName = "libmediainfo.so.0";
|
||||
}
|
||||
}
|
||||
|
||||
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Hosting.WindowsServices;
|
||||
using System.ServiceProcess;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Processes;
|
||||
|
||||
@@ -14,13 +14,16 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
private readonly Logger _logger;
|
||||
private readonly DateTime _startTime = DateTime.UtcNow;
|
||||
|
||||
public RuntimeInfo(IHostLifetime hostLifetime, Logger logger)
|
||||
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
IsWindowsService = hostLifetime is WindowsServiceLifetime;
|
||||
IsWindowsService = !IsUserInteractive &&
|
||||
OsInfo.IsWindows &&
|
||||
serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) &&
|
||||
serviceProvider.GetStatus(ServiceProvider.SERVICE_NAME) == ServiceControllerStatus.StartPending;
|
||||
|
||||
// net6.0 will return Radarr.dll for entry assembly, we need the actual
|
||||
// net5.0 will return Radarr.dll for entry assembly, we need the actual
|
||||
// executable name (Radarr on linux). On mono this will return the location of
|
||||
// the mono executable itself, which is not what we want.
|
||||
var entry = Process.GetCurrentProcess().MainModule;
|
||||
|
||||
@@ -6,6 +6,13 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
var knownKeys = new HashSet<TKey>();
|
||||
|
||||
return source.Where(element => knownKeys.Add(keySelector(element)));
|
||||
}
|
||||
|
||||
public static IEnumerable<TFirst> IntersectBy<TFirst, TSecond, TKey>(this IEnumerable<TFirst> first,
|
||||
Func<TFirst, TKey> firstKeySelector,
|
||||
IEnumerable<TSecond> second,
|
||||
|
||||
@@ -287,11 +287,6 @@ namespace NzbDrone.Common.Extensions
|
||||
return appFolderInfo.AppDataFolder;
|
||||
}
|
||||
|
||||
public static string GetDataProtectionPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), "asp");
|
||||
}
|
||||
|
||||
public static string GetLogFolder(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), "logs");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,6 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
public interface IHttpDispatcher
|
||||
{
|
||||
HttpResponse GetResponse(HttpRequest request, CookieContainer cookies);
|
||||
void DownloadFile(string url, string fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NLog.Fluent;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
|
||||
@@ -15,191 +14,200 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
{
|
||||
public class ManagedHttpDispatcher : IHttpDispatcher
|
||||
{
|
||||
private const string NO_PROXY_KEY = "no-proxy";
|
||||
|
||||
private const int connection_establish_timeout = 2000;
|
||||
private static bool useIPv6 = Socket.OSSupportsIPv6;
|
||||
private static bool hasResolvedIPv6Availability;
|
||||
|
||||
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
|
||||
private readonly ICreateManagedWebProxy _createManagedWebProxy;
|
||||
private readonly ICertificateValidationService _certificateValidationService;
|
||||
private readonly IUserAgentBuilder _userAgentBuilder;
|
||||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
||||
private readonly IPlatformInfo _platformInfo;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
||||
ICreateManagedWebProxy createManagedWebProxy,
|
||||
ICertificateValidationService certificateValidationService,
|
||||
IUserAgentBuilder userAgentBuilder,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger)
|
||||
{
|
||||
_proxySettingsProvider = proxySettingsProvider;
|
||||
_createManagedWebProxy = createManagedWebProxy;
|
||||
_certificateValidationService = certificateValidationService;
|
||||
_userAgentBuilder = userAgentBuilder;
|
||||
_platformInfo = platformInfo;
|
||||
_logger = logger;
|
||||
|
||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
||||
}
|
||||
|
||||
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
|
||||
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
|
||||
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
|
||||
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
|
||||
|
||||
var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
|
||||
if (cookieHeader.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
requestMessage.Headers.Add("Cookie", cookieHeader);
|
||||
}
|
||||
// Deflate is not a standard and could break depending on implementation.
|
||||
// we should just stick with the more compatible Gzip
|
||||
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
|
||||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
|
||||
webRequest.Method = request.Method.ToString();
|
||||
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
|
||||
webRequest.KeepAlive = request.ConnectionKeepAlive;
|
||||
webRequest.AllowAutoRedirect = false;
|
||||
webRequest.CookieContainer = cookies;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
if (request.RequestTimeout != TimeSpan.Zero)
|
||||
{
|
||||
cts.CancelAfter(request.RequestTimeout);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The default for System.Net.Http.HttpClient
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(100));
|
||||
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
|
||||
}
|
||||
|
||||
if (request.ContentData != null)
|
||||
{
|
||||
requestMessage.Content = new ByteArrayContent(request.ContentData);
|
||||
}
|
||||
webRequest.Proxy = GetProxy(request.Url);
|
||||
|
||||
if (request.Headers != null)
|
||||
{
|
||||
AddRequestHeaders(requestMessage, request.Headers);
|
||||
AddRequestHeaders(webRequest, request.Headers);
|
||||
}
|
||||
|
||||
var httpClient = GetClient(request.Url);
|
||||
|
||||
HttpResponseMessage responseMessage;
|
||||
HttpWebResponse httpWebResponse;
|
||||
|
||||
try
|
||||
{
|
||||
responseMessage = httpClient.Send(requestMessage, cts.Token);
|
||||
if (request.ContentData != null)
|
||||
{
|
||||
webRequest.ContentLength = request.ContentData.Length;
|
||||
using (var writeStream = webRequest.GetRequestStream())
|
||||
{
|
||||
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
|
||||
}
|
||||
}
|
||||
|
||||
httpWebResponse = (HttpWebResponse)webRequest.GetResponse();
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
catch (WebException e)
|
||||
{
|
||||
_logger.Error(e, "HttpClient error");
|
||||
throw;
|
||||
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
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte[] data = null;
|
||||
|
||||
using (var responseStream = responseMessage.Content.ReadAsStream())
|
||||
using (var responseStream = httpWebResponse.GetResponseStream())
|
||||
{
|
||||
if (responseStream != null && responseStream != Stream.Null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
// A target ResponseStream was specified, write to that instead.
|
||||
// But only on the OK status code, since we don't want to write failures and redirects.
|
||||
responseStream.CopyTo(request.ResponseStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = responseStream.ToBytes();
|
||||
}
|
||||
data = responseStream.ToBytes();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||
|
||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode);
|
||||
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
|
||||
}
|
||||
|
||||
protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri)
|
||||
public void DownloadFile(string url, string fileName)
|
||||
{
|
||||
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()
|
||||
try
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
|
||||
UseCookies = false, // sic - we don't want to use a shared cookie container
|
||||
AllowAutoRedirect = false,
|
||||
MaxConnectionsPerServer = 12,
|
||||
ConnectCallback = onConnect,
|
||||
SslOptions = new SslClientAuthenticationOptions
|
||||
var fileInfo = new FileInfo(fileName);
|
||||
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
|
||||
{
|
||||
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
|
||||
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);
|
||||
webClient.DownloadFile(uri.FullUri, fileName);
|
||||
stopWatch.Stop();
|
||||
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
|
||||
}
|
||||
}
|
||||
catch (WebException e)
|
||||
{
|
||||
_logger.Warn("Failed to get response from: {0} {1}", url, e.Message);
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warn(e, "Failed to get response from: " + url);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual IWebProxy GetProxy(HttpUri uri)
|
||||
{
|
||||
IWebProxy proxy = null;
|
||||
|
||||
var proxySettings = _proxySettingsProvider.GetProxySettings(uri);
|
||||
|
||||
if (proxySettings != null)
|
||||
{
|
||||
handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
|
||||
proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
|
||||
}
|
||||
|
||||
var client = new System.Net.Http.HttpClient(handler)
|
||||
{
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
|
||||
return client;
|
||||
return proxy;
|
||||
}
|
||||
|
||||
protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
|
||||
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
switch (header.Key)
|
||||
{
|
||||
case "Accept":
|
||||
webRequest.Headers.Accept.ParseAdd(header.Value);
|
||||
webRequest.Accept = header.Value;
|
||||
break;
|
||||
case "Connection":
|
||||
webRequest.Headers.Connection.Clear();
|
||||
webRequest.Headers.Connection.Add(header.Value);
|
||||
webRequest.Connection = header.Value;
|
||||
break;
|
||||
case "Content-Length":
|
||||
AddContentHeader(webRequest, "Content-Length", header.Value);
|
||||
webRequest.ContentLength = Convert.ToInt64(header.Value);
|
||||
break;
|
||||
case "Content-Type":
|
||||
AddContentHeader(webRequest, "Content-Type", header.Value);
|
||||
webRequest.ContentType = header.Value;
|
||||
break;
|
||||
case "Date":
|
||||
webRequest.Headers.Remove("Date");
|
||||
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
|
||||
webRequest.Date = HttpHeader.ParseDateTime(header.Value);
|
||||
break;
|
||||
case "Expect":
|
||||
webRequest.Headers.Expect.ParseAdd(header.Value);
|
||||
webRequest.Expect = header.Value;
|
||||
break;
|
||||
case "Host":
|
||||
webRequest.Headers.Host = header.Value;
|
||||
webRequest.Host = header.Value;
|
||||
break;
|
||||
case "If-Modified-Since":
|
||||
webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
|
||||
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
|
||||
break;
|
||||
case "Range":
|
||||
throw new NotImplementedException();
|
||||
case "Referer":
|
||||
webRequest.Headers.Add("Referer", header.Value);
|
||||
webRequest.Referer = header.Value;
|
||||
break;
|
||||
case "Transfer-Encoding":
|
||||
webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
|
||||
webRequest.TransferEncoding = header.Value;
|
||||
break;
|
||||
case "User-Agent":
|
||||
throw new NotSupportedException("User-Agent other than Radarr not allowed.");
|
||||
@@ -211,79 +219,5 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddContentHeader(HttpRequestMessage request, string header, string value)
|
||||
{
|
||||
var headers = request.Content?.Headers;
|
||||
if (headers == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
headers.Remove(header);
|
||||
headers.Add(header, value);
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
||||
if (useIPv6)
|
||||
{
|
||||
try
|
||||
{
|
||||
var localToken = cancellationToken;
|
||||
|
||||
if (!hasResolvedIPv6Availability)
|
||||
{
|
||||
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
|
||||
var quickFailCts = new CancellationTokenSource(connection_establish_timeout);
|
||||
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
|
||||
|
||||
localToken = linkedTokenSource.Token;
|
||||
}
|
||||
|
||||
return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
||||
useIPv6 = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
hasResolvedIPv6Availability = true;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to IPv4.
|
||||
return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> attemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
|
||||
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
|
||||
NoDelay = true
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// The stream should take the ownership of the underlying socket,
|
||||
// closing it when it's disposed.
|
||||
return new NetworkStream(socket, ownsSocket: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
socket.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/NzbDrone.Common/Http/GZipWebClient.cs
Normal file
15
src/NzbDrone.Common/Http/GZipWebClient.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -121,6 +119,8 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
|
||||
PrepareRequestCookies(request, cookieContainer);
|
||||
|
||||
var response = _httpDispatcher.GetResponse(request, cookieContainer);
|
||||
|
||||
HandleResponseCookies(response, cookieContainer);
|
||||
@@ -187,98 +187,57 @@ namespace NzbDrone.Common.Http
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleResponseCookies(HttpResponse response, CookieContainer container)
|
||||
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer)
|
||||
{
|
||||
foreach (Cookie cookie in container.GetAllCookies())
|
||||
// Don't collect persistnet cookies for intermediate/redirected urls.
|
||||
/*lock (_cookieContainerCache)
|
||||
{
|
||||
cookie.Expired = true;
|
||||
}
|
||||
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
|
||||
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
|
||||
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
|
||||
|
||||
cookieContainer.Add(persistentCookies);
|
||||
cookieContainer.Add(existingCookies);
|
||||
}*/
|
||||
}
|
||||
|
||||
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer)
|
||||
{
|
||||
var cookieHeaders = response.GetCookieHeaders();
|
||||
|
||||
if (cookieHeaders.Empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
|
||||
|
||||
if (response.Request.StoreResponseCookie)
|
||||
{
|
||||
lock (_cookieContainerCache)
|
||||
{
|
||||
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
|
||||
|
||||
AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
|
||||
{
|
||||
foreach (var cookieHeader in cookieHeaders)
|
||||
{
|
||||
try
|
||||
{
|
||||
container.SetCookies((Uri)url, cookieHeader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex, "Invalid cookie in {0}", url);
|
||||
foreach (var cookieHeader in cookieHeaders)
|
||||
{
|
||||
try
|
||||
{
|
||||
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DownloadFile(string url, string fileName)
|
||||
{
|
||||
var fileNamePart = fileName + ".part";
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(fileName);
|
||||
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
|
||||
{
|
||||
fileInfo.Directory.Create();
|
||||
}
|
||||
|
||||
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
|
||||
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
|
||||
{
|
||||
var request = new HttpRequest(url);
|
||||
request.AllowAutoRedirect = true;
|
||||
request.ResponseStream = fileStream;
|
||||
var response = Get(request);
|
||||
|
||||
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
|
||||
{
|
||||
throw new HttpException(request, response, "Site responded with html content.");
|
||||
}
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
|
||||
if (File.Exists(fileName))
|
||||
{
|
||||
File.Delete(fileName);
|
||||
}
|
||||
|
||||
File.Move(fileNamePart, fileName);
|
||||
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(fileNamePart))
|
||||
{
|
||||
File.Delete(fileNamePart);
|
||||
}
|
||||
}
|
||||
_httpDispatcher.DownloadFile(url, fileName);
|
||||
}
|
||||
|
||||
public HttpResponse Get(HttpRequest request)
|
||||
{
|
||||
request.Method = HttpMethod.Get;
|
||||
request.Method = HttpMethod.GET;
|
||||
return Execute(request);
|
||||
}
|
||||
|
||||
@@ -292,13 +251,13 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public HttpResponse Head(HttpRequest request)
|
||||
{
|
||||
request.Method = HttpMethod.Head;
|
||||
request.Method = HttpMethod.HEAD;
|
||||
return Execute(request);
|
||||
}
|
||||
|
||||
public HttpResponse Post(HttpRequest request)
|
||||
{
|
||||
request.Method = HttpMethod.Post;
|
||||
request.Method = HttpMethod.POST;
|
||||
return Execute(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
public static class WebHeaderCollectionExtensions
|
||||
{
|
||||
public static NameValueCollection ToNameValueCollection(this HttpHeaders headers)
|
||||
{
|
||||
var result = new NameValueCollection();
|
||||
foreach (var header in headers)
|
||||
{
|
||||
result.Add(header.Key, header.Value.ConcatToString(";"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
|
||||
{
|
||||
public HttpHeader(NameValueCollection headers)
|
||||
@@ -32,11 +16,6 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
}
|
||||
|
||||
public HttpHeader(HttpHeaders headers)
|
||||
: base(headers.ToNameValueCollection())
|
||||
{
|
||||
}
|
||||
|
||||
public HttpHeader()
|
||||
{
|
||||
}
|
||||
|
||||
14
src/NzbDrone.Common/Http/HttpMethod.cs
Normal file
14
src/NzbDrone.Common/Http/HttpMethod.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
public enum HttpMethod
|
||||
{
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
PATCH,
|
||||
MERGE
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -13,7 +11,6 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
public HttpRequest(string url, HttpAccept httpAccept = null)
|
||||
{
|
||||
Method = HttpMethod.Get;
|
||||
Url = new HttpUri(url);
|
||||
Headers = new HttpHeader();
|
||||
AllowAutoRedirect = true;
|
||||
@@ -52,7 +49,6 @@ namespace NzbDrone.Common.Http
|
||||
public TimeSpan RequestTimeout { get; set; }
|
||||
public TimeSpan RateLimit { get; set; }
|
||||
public string RateLimitKey { get; set; }
|
||||
public Stream ResponseStream { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
@@ -36,7 +35,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
BaseUrl = new HttpUri(baseUrl);
|
||||
ResourceUrl = string.Empty;
|
||||
Method = HttpMethod.Get;
|
||||
Method = HttpMethod.GET;
|
||||
QueryParams = new List<KeyValuePair<string, string>>();
|
||||
SuffixQueryParams = new List<KeyValuePair<string, string>>();
|
||||
Segments = new Dictionary<string, string>();
|
||||
@@ -272,7 +271,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public virtual HttpRequestBuilder Post()
|
||||
{
|
||||
Method = HttpMethod.Post;
|
||||
Method = HttpMethod.POST;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -363,7 +362,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public virtual HttpRequestBuilder AddFormParameter(string key, object value)
|
||||
{
|
||||
if (Method != HttpMethod.Post)
|
||||
if (Method != HttpMethod.POST)
|
||||
{
|
||||
throw new NotSupportedException("HttpRequest Method must be POST to add FormParameter.");
|
||||
}
|
||||
@@ -379,7 +378,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public virtual HttpRequestBuilder AddFormUpload(string name, string fileName, byte[] data, string contentType = "application/octet-stream")
|
||||
{
|
||||
if (Method != HttpMethod.Post)
|
||||
if (Method != HttpMethod.POST)
|
||||
{
|
||||
throw new NotSupportedException("HttpRequest Method must be POST to add FormUpload.");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
@@ -18,14 +17,14 @@ namespace NzbDrone.Common.Http
|
||||
public JsonRpcRequestBuilder(string baseUrl)
|
||||
: base(baseUrl)
|
||||
{
|
||||
Method = HttpMethod.Post;
|
||||
Method = HttpMethod.POST;
|
||||
JsonParameters = new List<object>();
|
||||
}
|
||||
|
||||
public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable<object> parameters)
|
||||
: base(baseUrl)
|
||||
{
|
||||
Method = HttpMethod.Post;
|
||||
Method = HttpMethod.POST;
|
||||
JsonMethod = method;
|
||||
JsonParameters = parameters.ToList();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using com.LandonKey.SocksWebProxy;
|
||||
using com.LandonKey.SocksWebProxy.Proxy;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
@@ -29,37 +33,54 @@ namespace NzbDrone.Common.Http.Proxy
|
||||
}
|
||||
|
||||
private IWebProxy CreateWebProxy(HttpProxySettings proxySettings)
|
||||
{
|
||||
var uri = GetProxyUri(proxySettings);
|
||||
|
||||
if (uri == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri GetProxyUri(HttpProxySettings proxySettings)
|
||||
{
|
||||
switch (proxySettings.Type)
|
||||
{
|
||||
case ProxyType.Http:
|
||||
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
|
||||
}
|
||||
|
||||
case ProxyType.Socks4:
|
||||
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Four, proxySettings.Username, proxySettings.Password), false);
|
||||
case ProxyType.Socks5:
|
||||
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
default:
|
||||
return null;
|
||||
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Five, proxySettings.Username, proxySettings.Password), false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IPAddress GetProxyIpAddress(string host)
|
||||
{
|
||||
IPAddress ipAddress;
|
||||
if (!IPAddress.TryParse(host, out ipAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
|
||||
}
|
||||
}
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
private static int GetNextFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&|: )(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="4.7.12" />
|
||||
<PackageReference Include="DryIoc.dll" Version="4.7.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.11" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="Sentry" Version="3.11.1" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Sentry" Version="3.9.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
|
||||
<ApplicationIcon>..\NzbDrone.Host\Radarr.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="!$(RuntimeIdentifier.StartsWith('win'))">
|
||||
<AssemblyName>Radarr</AssemblyName>
|
||||
|
||||
14
src/NzbDrone.Console/app.config
Normal file
14
src/NzbDrone.Console/app.config
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<system.net>
|
||||
<connectionManagement>
|
||||
<add address="*" maxconnection="100" />
|
||||
</connectionManagement>
|
||||
</system.net>
|
||||
<startup useLegacyV2RuntimeActivationPolicy="true">
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<loadFromRemoteSources enabled="true" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
31
src/NzbDrone.Console/app.manifest
Normal file
31
src/NzbDrone.Console/app.manifest
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -1,131 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Download.Clients.RTorrent;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class cdh_per_downloadclientFixture : MigrationTest<cdh_per_downloadclient>
|
||||
{
|
||||
[Test]
|
||||
public void should_set_cdh_to_enabled()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("DownloadClients").Row(new
|
||||
{
|
||||
Enable = 1,
|
||||
Name = "Deluge",
|
||||
Implementation = "Deluge",
|
||||
Priority = 1,
|
||||
Settings = new DelugeSettings85
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
MovieCategory = "abc",
|
||||
UrlBase = "/my/"
|
||||
}.ToJson(),
|
||||
ConfigContract = "DelugeSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().RemoveCompletedDownloads.Should().BeFalse();
|
||||
items.First().RemoveFailedDownloads.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_cdh_to_disabled_when_globally_disabled()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Config").Row(new
|
||||
{
|
||||
Key = "removecompleteddownloads",
|
||||
Value = "True"
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("DownloadClients").Row(new
|
||||
{
|
||||
Enable = 1,
|
||||
Name = "Deluge",
|
||||
Implementation = "Deluge",
|
||||
Priority = 1,
|
||||
Settings = new DelugeSettings85
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
MovieCategory = "abc",
|
||||
UrlBase = "/my/"
|
||||
}.ToJson(),
|
||||
ConfigContract = "DelugeSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().RemoveCompletedDownloads.Should().BeTrue();
|
||||
items.First().RemoveFailedDownloads.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_disable_remove_for_existing_rtorrent()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("DownloadClients").Row(new
|
||||
{
|
||||
Enable = 1,
|
||||
Name = "RTorrent",
|
||||
Implementation = "RTorrent",
|
||||
Priority = 1,
|
||||
Settings = new RTorrentSettings
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
MovieCategory = "abc",
|
||||
UrlBase = "/my/"
|
||||
}.ToJson(),
|
||||
ConfigContract = "RTorrentSettings"
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().RemoveCompletedDownloads.Should().BeFalse();
|
||||
items.First().RemoveFailedDownloads.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
public class DownloadClientDefinition158
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool Enable { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public JsonElement Settings { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public bool RemoveCompletedDownloads { get; set; }
|
||||
public bool RemoveFailedDownloads { get; set; }
|
||||
}
|
||||
|
||||
public class DelugeSettings85
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string UrlBase { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string MovieCategory { get; set; }
|
||||
public int RecentTvPriority { get; set; }
|
||||
public int OlderTvPriority { get; set; }
|
||||
public bool UseSsl { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
var remoteMovie = new RemoteMovie();
|
||||
remoteMovie.ParsedMovieInfo = new ParsedMovieInfo();
|
||||
remoteMovie.ParsedMovieInfo.MovieTitles = new List<string> { "A Movie" };
|
||||
remoteMovie.ParsedMovieInfo.MovieTitle = "A Movie";
|
||||
remoteMovie.ParsedMovieInfo.Year = 1998;
|
||||
remoteMovie.ParsedMovieInfo.Quality = quality;
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -32,13 +31,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
};
|
||||
|
||||
Mocker
|
||||
.GetMock<IIndexerFactory>()
|
||||
.GetMock<IIndexerRepository>()
|
||||
.Setup(m => m.Get(It.IsAny<int>()))
|
||||
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), -1));
|
||||
|
||||
Mocker
|
||||
.GetMock<IIndexerFactory>()
|
||||
.Setup(m => m.Get(1))
|
||||
.Returns(_fakeIndexerDefinition);
|
||||
|
||||
_specification = Mocker.Resolve<IndexerTagSpecification>();
|
||||
@@ -104,25 +98,5 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void release_without_indexerid_should_return_true()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
|
||||
_fakeMovie.Tags = new HashSet<int> { 123, 789 };
|
||||
_fakeRelease.IndexerId = 0;
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void release_with_invalid_indexerid_should_return_true()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
|
||||
_fakeMovie.Tags = new HashSet<int> { 123, 789 };
|
||||
_fakeRelease.IndexerId = 2;
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
{
|
||||
Quality = quality,
|
||||
Year = 1998,
|
||||
MovieTitles = new List<string> { "A Movie" },
|
||||
MovieTitle = "A Movie",
|
||||
},
|
||||
Movie = movie,
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||
_pending.Add(new PendingRelease
|
||||
{
|
||||
Id = id,
|
||||
ParsedMovieInfo = new ParsedMovieInfo { MovieTitles = new List<string> { title }, Year = year },
|
||||
ParsedMovieInfo = new ParsedMovieInfo { MovieTitle = title, Year = year },
|
||||
MovieId = _movie.Id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||
|
||||
ParsedMovieInfo = new ParsedMovieInfo()
|
||||
{
|
||||
MovieTitles = new List<string> { "A Movie" },
|
||||
MovieTitle = "A Movie",
|
||||
Year = 1998
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.Is<ParsedMovieInfo>(i => i.PrimaryMovieTitle == "A Movie"), It.IsAny<string>(), null))
|
||||
.Setup(s => s.Map(It.Is<ParsedMovieInfo>(i => i.MovieTitle == "A Movie"), It.IsAny<string>(), null))
|
||||
.Returns(new MappingResult { RemoteMovie = remoteEpisode });
|
||||
|
||||
ParseMovieTitle();
|
||||
@@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||
|
||||
ParsedMovieInfo = new ParsedMovieInfo()
|
||||
{
|
||||
MovieTitles = { "A Movie" },
|
||||
MovieTitle = "A Movie",
|
||||
Year = 1998
|
||||
}
|
||||
};
|
||||
@@ -156,7 +156,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||
|
||||
ParsedMovieInfo = new ParsedMovieInfo()
|
||||
{
|
||||
MovieTitles = { "A Movie" },
|
||||
MovieTitle = "A Movie",
|
||||
Year = 1998
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>(), TestLogger));
|
||||
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<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
|
||||
}
|
||||
|
||||
@@ -70,5 +70,84 @@ namespace NzbDrone.Core.Test.HistoryTests
|
||||
|
||||
downloadHistory.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_movie_history()
|
||||
{
|
||||
var historyMovie1 = Builder<MovieHistory>.CreateNew()
|
||||
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
|
||||
.With(c => c.Languages = new List<Language> { Language.English })
|
||||
.With(c => c.MovieId = 12)
|
||||
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
|
||||
.BuildNew();
|
||||
|
||||
var historyMovie2 = Builder<MovieHistory>.CreateNew()
|
||||
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
|
||||
.With(c => c.Languages = new List<Language> { Language.English })
|
||||
.With(c => c.MovieId = 13)
|
||||
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
|
||||
.BuildNew();
|
||||
|
||||
Subject.Insert(historyMovie1);
|
||||
Subject.Insert(historyMovie2);
|
||||
|
||||
var movieHistory = Subject.GetByMovieId(12, null);
|
||||
|
||||
movieHistory.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_sort_movie_history_by_date()
|
||||
{
|
||||
var historyFirst = Builder<MovieHistory>.CreateNew()
|
||||
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
|
||||
.With(c => c.Languages = new List<Language> { Language.English })
|
||||
.With(c => c.MovieId = 12)
|
||||
.With(c => c.EventType = MovieHistoryEventType.MovieFileRenamed)
|
||||
.With(c => c.Date = DateTime.UtcNow)
|
||||
.BuildNew();
|
||||
|
||||
var historySecond = Builder<MovieHistory>.CreateNew()
|
||||
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
|
||||
.With(c => c.Languages = new List<Language> { Language.English })
|
||||
.With(c => c.MovieId = 12)
|
||||
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
|
||||
.With(c => c.Date = DateTime.UtcNow.AddMinutes(10))
|
||||
.BuildNew();
|
||||
|
||||
Subject.Insert(historyFirst);
|
||||
Subject.Insert(historySecond);
|
||||
|
||||
var movieHistory = Subject.GetByMovieId(12, null);
|
||||
|
||||
movieHistory.Should().HaveCount(2);
|
||||
movieHistory.First().EventType.Should().Be(MovieHistoryEventType.Grabbed);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_history_items_by_movieId()
|
||||
{
|
||||
var items = Builder<MovieHistory>.CreateListOfSize(5)
|
||||
.TheFirst(1)
|
||||
.With(c => c.MovieId = _movie2.Id)
|
||||
.TheRest()
|
||||
.With(c => c.MovieId = _movie1.Id)
|
||||
.All()
|
||||
.With(c => c.Id = 0)
|
||||
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
|
||||
.With(c => c.Languages = new List<Language> { Language.English })
|
||||
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
|
||||
.BuildListOfNew();
|
||||
|
||||
Db.InsertMany(items);
|
||||
|
||||
Subject.DeleteForMovies(new List<int> { _movie1.Id });
|
||||
|
||||
var removedItems = Subject.GetByMovieId(_movie1.Id, null);
|
||||
var nonRemovedItems = Subject.GetByMovieId(_movie2.Id, null);
|
||||
|
||||
removedItems.Should().HaveCount(0);
|
||||
nonRemovedItems.Should().HaveCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Translations;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||
{
|
||||
public class ReleaseSearchServiceFixture : CoreTest<ReleaseSearchService>
|
||||
{
|
||||
private Mock<IIndexer> _mockIndexer;
|
||||
private Movie _movie;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_mockIndexer = Mocker.GetMock<IIndexer>();
|
||||
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = 1 });
|
||||
_mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true);
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(s => s.AutomaticSearchEnabled(true))
|
||||
.Returns(new List<IIndexer> { _mockIndexer.Object });
|
||||
|
||||
Mocker.GetMock<IMakeDownloadDecision>()
|
||||
.Setup(s => s.GetSearchDecision(It.IsAny<List<Parser.Model.ReleaseInfo>>(), It.IsAny<SearchCriteriaBase>()))
|
||||
.Returns(new List<DownloadDecision>());
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(v => v.Monitored = true)
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Setup(v => v.GetMovie(_movie.Id))
|
||||
.Returns(_movie);
|
||||
|
||||
Mocker.GetMock<IMovieTranslationService>()
|
||||
.Setup(s => s.GetAllTranslationsForMovie(It.IsAny<int>()))
|
||||
.Returns(new List<MovieTranslation>());
|
||||
}
|
||||
|
||||
private List<SearchCriteriaBase> WatchForSearchCriteria()
|
||||
{
|
||||
var result = new List<SearchCriteriaBase>();
|
||||
|
||||
_mockIndexer.Setup(v => v.Fetch(It.IsAny<MovieSearchCriteria>()))
|
||||
.Callback<MovieSearchCriteria>(s => result.Add(s))
|
||||
.Returns(new List<Parser.Model.ReleaseInfo>());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Tags_IndexerTags_MovieNoTags_IndexerNotIncluded()
|
||||
{
|
||||
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
|
||||
{
|
||||
Id = 1,
|
||||
Tags = new HashSet<int> { 3 }
|
||||
});
|
||||
|
||||
var allCriteria = WatchForSearchCriteria();
|
||||
|
||||
Subject.MovieSearch(_movie, true, false);
|
||||
|
||||
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
|
||||
|
||||
criteria.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Tags_IndexerNoTags_MovieTags_IndexerIncluded()
|
||||
{
|
||||
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
|
||||
{
|
||||
Id = 1
|
||||
});
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(v => v.Monitored = true)
|
||||
.With(v => v.Tags = new HashSet<int> { 3 })
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Setup(v => v.GetMovie(_movie.Id))
|
||||
.Returns(_movie);
|
||||
|
||||
var allCriteria = WatchForSearchCriteria();
|
||||
|
||||
Subject.MovieSearch(_movie, true, false);
|
||||
|
||||
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
|
||||
|
||||
criteria.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Tags_IndexerAndMovieTagsMatch_IndexerIncluded()
|
||||
{
|
||||
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
|
||||
{
|
||||
Id = 1,
|
||||
Tags = new HashSet<int> { 1, 2, 3 }
|
||||
});
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(v => v.Monitored = true)
|
||||
.With(v => v.Tags = new HashSet<int> { 3, 4, 5 })
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Setup(v => v.GetMovie(_movie.Id))
|
||||
.Returns(_movie);
|
||||
|
||||
var allCriteria = WatchForSearchCriteria();
|
||||
|
||||
Subject.MovieSearch(_movie, true, false);
|
||||
|
||||
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
|
||||
|
||||
criteria.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Tags_IndexerAndMovieTagsMismatch_IndexerNotIncluded()
|
||||
{
|
||||
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
|
||||
{
|
||||
Id = 1,
|
||||
Tags = new HashSet<int> { 1, 2, 3 }
|
||||
});
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(v => v.Monitored = true)
|
||||
.With(v => v.Tags = new HashSet<int> { 4, 5, 6 })
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Setup(v => v.GetMovie(_movie.Id))
|
||||
.Returns(_movie);
|
||||
|
||||
var allCriteria = WatchForSearchCriteria();
|
||||
|
||||
Subject.MovieSearch(_movie, true, false);
|
||||
|
||||
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
|
||||
|
||||
criteria.Count.Should().Be(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||
public void should_replace_some_special_characters(string input, string expected)
|
||||
{
|
||||
Subject.SceneTitles = new List<string> { input };
|
||||
Subject.CleanSceneTitles.First().Should().Be(expected);
|
||||
Subject.QueryTitles.First().Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -31,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/FileList/RecentFeed.json");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
@@ -35,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
|
||||
var responseJson = ReadAllText(fileName);
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), responseJson));
|
||||
|
||||
var torrents = Subject.FetchRecent();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -89,7 +88,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/IPTorrents/IPTorrents.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -42,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -31,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -34,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OmgwtfnzbsTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -35,11 +34,11 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
|
||||
var responseJson = ReadAllText(fileName);
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), authStream.ToString()));
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, responseJson));
|
||||
|
||||
var torrents = Subject.FetchRecent();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -36,7 +35,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
@@ -63,7 +62,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
public void should_parse_error_20_as_empty_results()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 20, error: \"some message\" }"));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
@@ -75,7 +74,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
public void should_warn_on_unknown_error()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 25, error: \"some message\" }"));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -43,7 +42,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
@@ -72,7 +71,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
@@ -102,7 +101,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_animetosho.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
@@ -45,10 +45,6 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Mocker.GetMock<IRootFolderService>()
|
||||
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
|
||||
.Returns(_rootFolder);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(s => s.GetFilesByMovie(It.IsAny<int>()))
|
||||
.Returns(new List<MovieFile>());
|
||||
}
|
||||
|
||||
private void GivenRootFolder(params string[] subfolders)
|
||||
@@ -121,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
.Verify(v => v.Clean(It.IsAny<Movie>(), It.IsAny<List<string>>()), Times.Never());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie, false), Times.Never());
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -156,7 +152,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -181,7 +177,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -201,7 +197,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -233,7 +229,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
.Verify(v => v.Clean(It.IsAny<Movie>(), It.IsAny<List<string>>()), Times.Once());
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie, false), Times.Never());
|
||||
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -251,7 +247,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -274,7 +270,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -293,7 +289,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -313,7 +309,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -330,7 +326,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -347,7 +343,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -366,7 +362,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -383,7 +379,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -401,7 +397,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -418,7 +414,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
||||
Subject.Scan(_movie);
|
||||
|
||||
Mocker.GetMock<IMakeImportDecision>()
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
|
||||
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
namespace NzbDrone.Core.Test.MediaFiles
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
@@ -76,13 +76,6 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
_approvedDecisions.ForEach(a => a.LocalMovie.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), Path.GetFileName(a.LocalMovie.Path)));
|
||||
}
|
||||
|
||||
private void GivenExistingFileOnDisk()
|
||||
{
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
|
||||
.Returns(new List<MovieFile>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_import_any_if_there_are_no_approved_decisions()
|
||||
{
|
||||
@@ -94,16 +87,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
[Test]
|
||||
public void should_import_each_approved()
|
||||
{
|
||||
GivenExistingFileOnDisk();
|
||||
|
||||
Subject.Import(_approvedDecisions, false).Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_only_import_approved()
|
||||
{
|
||||
GivenExistingFileOnDisk();
|
||||
|
||||
var all = new List<ImportDecision>();
|
||||
all.AddRange(_rejectedDecisions);
|
||||
all.AddRange(_approvedDecisions);
|
||||
@@ -115,10 +104,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_only_import_each_movie_once()
|
||||
public void should_only_import_each_episode_once()
|
||||
{
|
||||
GivenExistingFileOnDisk();
|
||||
|
||||
var all = new List<ImportDecision>();
|
||||
all.AddRange(_approvedDecisions);
|
||||
all.Add(new ImportDecision(_approvedDecisions.First().LocalMovie));
|
||||
@@ -150,8 +137,6 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
[Test]
|
||||
public void should_not_move_existing_files()
|
||||
{
|
||||
GivenExistingFileOnDisk();
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
|
||||
|
||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||
@@ -240,8 +225,6 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
[Test]
|
||||
public void should_import_larger_files_first()
|
||||
{
|
||||
GivenExistingFileOnDisk();
|
||||
|
||||
var fileDecision = _approvedDecisions.First();
|
||||
fileDecision.LocalMovie.Size = 1.Gigabytes();
|
||||
|
||||
@@ -338,10 +321,10 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
{
|
||||
var name = "Transformers.2007.720p.BluRay.x264-Radarr";
|
||||
var outputPath = Path.Combine(@"C:\Test\Unsorted\movies\".AsOsAgnostic(), name);
|
||||
var localMovie = _approvedDecisions.First().LocalMovie;
|
||||
var localEpisode = _approvedDecisions.First().LocalMovie;
|
||||
|
||||
localMovie.FolderMovieInfo = new ParsedMovieInfo { OriginalTitle = name };
|
||||
localMovie.Path = Path.Combine(outputPath, "subfolder", name + ".mkv");
|
||||
localEpisode.FolderMovieInfo = new ParsedMovieInfo { OriginalTitle = name };
|
||||
localEpisode.Path = Path.Combine(outputPath, "subfolder", name + ".mkv");
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, null);
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class FormatAudioChannelsFixture : TestBase
|
||||
{
|
||||
[Test]
|
||||
public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 6,
|
||||
AudioChannelPositions = null,
|
||||
AudioChannelPositionsTextContainer = "Front: L C R, Side: L R, LFE"
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = null,
|
||||
AudioChannelPositionsTextContainer = "Front: L R"
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = null,
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 2
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = null,
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_AudioChannels_if_schema_revision_is_3_and_AudioChannelPositions_is_0()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioFormat = "FLAC",
|
||||
AudioChannelsContainer = 6,
|
||||
AudioChannelPositions = "0/0/0",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_sum_AudioChannelPositions()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "2/0/0",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_sum_AudioChannelPositions_including_decimal()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "3/2/0.1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_format_8_channel_object_based_as_71_if_dtsx()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 8,
|
||||
AudioChannelsStream = 0,
|
||||
AudioFormat = "DTS",
|
||||
AudioAdditionalFeatures = "XLL X",
|
||||
AudioChannelPositions = "Object Based",
|
||||
AudioChannelPositionsTextContainer = "Object Based",
|
||||
AudioChannelPositionsTextStream = "Object Based",
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_format_8_channel_blank_as_71_if_dtsx()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 8,
|
||||
AudioChannelsStream = 0,
|
||||
AudioFormat = "DTS",
|
||||
AudioAdditionalFeatures = "XLL X",
|
||||
AudioChannelPositions = "",
|
||||
AudioChannelPositionsTextContainer = "",
|
||||
AudioChannelPositionsTextStream = "Object Based",
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_format_6_channel_zero_as_51_if_flac()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioFormat = "FLAC",
|
||||
AudioChannelsContainer = 6,
|
||||
AudioChannelPositions = "0/0/0",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_ignore_culture_on_channel_summary()
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");
|
||||
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "3/2/0.1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_handle_AudioChannelPositions_three_digits()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "3/2/0.2.1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_cleanup_extraneous_text_from_AudioChannelPositions()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "Object Based / 3/2/2.1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_empty_groups_in_AudioChannelPositions()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = " / 2/0/0.0",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_sum_first_series_of_numbers_from_AudioChannelPositions()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "3/2/2.1 / 3/2/2.1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_sum_first_series_of_numbers_from_AudioChannelPositions_with_three_digits()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "3/2/0.2.1 / 3/2/0.1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_sum_dual_mono_representation_AudioChannelPositions()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "1+1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_AudioChannelPositionText_when_AudioChannelChannelPosition_is_invalid()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 6,
|
||||
AudioChannelPositions = "15 objects",
|
||||
AudioChannelPositionsTextContainer = "15 objects / Front: L C R, Side: L R, LFE",
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_remove_atmos_objects_from_AudioChannelPostions()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 2,
|
||||
AudioChannelPositions = "15 objects / 3/2.1",
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_audio_stream_text_when_exists()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 6,
|
||||
AudioChannelsStream = 8,
|
||||
AudioChannelPositions = null,
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
AudioChannelPositionsTextStream = "Front: L C R, Side: L R, Back: L R, LFE",
|
||||
SchemaRevision = 6
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_audio_stream_channels_when_exists()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioChannelsContainer = 6,
|
||||
AudioChannelsStream = 8,
|
||||
AudioChannelPositions = null,
|
||||
AudioChannelPositionsTextContainer = null,
|
||||
AudioChannelPositionsTextStream = null,
|
||||
SchemaRevision = 6
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(8m);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,31 +10,45 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
|
||||
{
|
||||
private static string sceneName = "My.Series.S01E01-Sonarr";
|
||||
|
||||
[TestCase("mp2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")]
|
||||
[TestCase("vorbis, , ", "DB Super HDTV", "Vorbis")]
|
||||
[TestCase("pcm_s16le, , ", "DW DVDRip XviD-idTV, ", "PCM")]
|
||||
[TestCase("truehd, , ", "", "TrueHD")]
|
||||
[TestCase("truehd, , ", "TrueHD", "TrueHD")]
|
||||
[TestCase("truehd, thd+, ", "Atmos", "TrueHD Atmos")]
|
||||
[TestCase("truehd, thd+, ", "TrueHD.Atmos.7.1", "TrueHD Atmos")]
|
||||
[TestCase("truehd, thd+, ", "", "TrueHD Atmos")]
|
||||
[TestCase("wmav1, , ", "Droned.wmv", "WMA")]
|
||||
[TestCase("wmav2, , ", "B.N.S04E18.720p.WEB-DL", "WMA")]
|
||||
[TestCase("opus, , ", "Roadkill Ep3x11 - YouTube.webm", "Opus")]
|
||||
[TestCase("mp3, , ", "climbing.mp4", "MP3")]
|
||||
[TestCase("dts, , DTS-HD MA", "DTS-HD.MA", "DTS-HD MA")]
|
||||
[TestCase("dts, , DTS:X", "DTS-X", "DTS-X")]
|
||||
[TestCase("dts, , DTS-HD MA", "DTS-HD.MA", "DTS-HD MA")]
|
||||
[TestCase("dts, , DTS-ES", "DTS-ES", "DTS-ES")]
|
||||
[TestCase("dts, , DTS-ES", "DTS", "DTS-ES")]
|
||||
[TestCase("dts, , DTS-HD HRA", "DTSHD-HRA", "DTS-HD HRA")]
|
||||
[TestCase("dts, , DTS", "DTS", "DTS")]
|
||||
[TestCase("eac3, ec+3, ", "EAC3.Atmos", "EAC3 Atmos")]
|
||||
[TestCase("eac3, , ", "DDP5.1", "EAC3")]
|
||||
[TestCase("ac3, , ", "DD5.1", "AC3")]
|
||||
[TestCase("adpcm_ima_qt, , ", "Custom?", "PCM")]
|
||||
[TestCase("adpcm_ms, , ", "Custom", "PCM")]
|
||||
[TestCase("AC-3", "AC3")]
|
||||
[TestCase("E-AC-3", "EAC3")]
|
||||
[TestCase("MPEG Audio", "MPEG Audio")]
|
||||
[TestCase("DTS", "DTS")]
|
||||
public void should_format_audio_format_legacy(string audioFormat, string expectedFormat)
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioFormat = audioFormat
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
|
||||
}
|
||||
|
||||
[TestCase("MPEG Audio, A_MPEG/L2, , , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")]
|
||||
[TestCase("Vorbis, A_VORBIS, , Xiph.Org libVorbis I 20101101 (Schaufenugget), ", "DB Super HDTV", "Vorbis")]
|
||||
[TestCase("PCM, 1, , , ", "DW DVDRip XviD-idTV, ", "PCM")] // Dubbed most likely
|
||||
[TestCase("TrueHD, A_TRUEHD, , , ", "", "TrueHD")]
|
||||
[TestCase("MLP FBA, A_TRUEHD, , , ", "TrueHD", "TrueHD")]
|
||||
[TestCase("MLP FBA, A_TRUEHD, , , 16-ch", "Atmos", "TrueHD Atmos")]
|
||||
[TestCase("Atmos / TrueHD, A_TRUEHD, , , ", "TrueHD.Atmos.7.1", "TrueHD Atmos")]
|
||||
[TestCase("Atmos / TrueHD / AC-3, 131, , , ", "", "TrueHD Atmos")]
|
||||
[TestCase("WMA, 161, , , ", "Droned.wmv", "WMA")]
|
||||
[TestCase("WMA, 162, Pro, , ", "B.N.S04E18.720p.WEB-DL", "WMA")]
|
||||
[TestCase("Opus, A_OPUS, , , ", "Roadkill Ep3x11 - YouTube.webm", "Opus")]
|
||||
[TestCase("mp3 , 0, , , ", "climbing.mp4", "MP3")]
|
||||
[TestCase("DTS, A_DTS, , , XLL", "DTS-HD.MA", "DTS-HD MA")]
|
||||
[TestCase("DTS, A_DTS, , , XLL X", "DTS-X", "DTS-X")]
|
||||
[TestCase("DTS, A_DTS, , , ES XLL", "DTS-HD.MA", "DTS-HD MA")]
|
||||
[TestCase("DTS, A_DTS, , , ES", "DTS-ES", "DTS-ES")]
|
||||
[TestCase("DTS, A_DTS, , , ES XXCH", "DTS", "DTS-ES")]
|
||||
[TestCase("DTS, A_DTS, , , XBR", "DTSHD-HRA", "DTS-HD HRA")]
|
||||
[TestCase("DTS, A_DTS, , , DTS", "DTS", "DTS")]
|
||||
[TestCase("E-AC-3, A_EAC3, , , JOC", "EAC3.Atmos", "EAC3 Atmos")]
|
||||
[TestCase("E-AC-3, A_EAC3, , , ", "DDP5.1", "EAC3")]
|
||||
[TestCase("AC-3, A_AC3, , , ", "DD5.1", "AC3")]
|
||||
[TestCase("A_QUICKTIME, A_QUICKTIME, , , ", "", "")]
|
||||
[TestCase("ADPCM, 2, , , ", "Custom?", "PCM")]
|
||||
[TestCase("ADPCM, ima4, , , ", "Custom", "PCM")]
|
||||
public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat)
|
||||
{
|
||||
var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
|
||||
@@ -42,12 +56,26 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
|
||||
{
|
||||
AudioFormat = split[0],
|
||||
AudioCodecID = split[1],
|
||||
AudioProfile = split[2]
|
||||
AudioProfile = split[2],
|
||||
AudioCodecLibrary = split[3],
|
||||
AudioAdditionalFeatures = split[4]
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_MP3_for_MPEG_Audio_with_Layer_3_for_the_profile()
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
AudioFormat = "MPEG Audio",
|
||||
AudioProfile = "Layer 3"
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be("MP3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_AudioFormat_by_default()
|
||||
{
|
||||
|
||||
@@ -8,55 +8,94 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
|
||||
[TestFixture]
|
||||
public class FormatVideoCodecFixture : TestBase
|
||||
{
|
||||
[TestCase("mpeg2video, ", "Droned.S01E02.1080i.HDTV.DD5.1.MPEG2-NTb", "MPEG2")]
|
||||
[TestCase("mpeg2video, ", "", "MPEG2")]
|
||||
[TestCase("mpeg1video, ", "The.Simpsons.S13E04.INTERNAL-ANiVCD.mpg", "MPEG")]
|
||||
[TestCase("vc1, WVC1", "B.N.S04E18.720p.WEB-DL", "VC1")]
|
||||
[TestCase("vc1, V_MS/VFW/FOURCC/WVC1", "", "VC1")]
|
||||
[TestCase("vc1, WMV3", "It's Always Sunny S07E13 The Gang's RevengeHDTV.XviD-2HD.avi", "VC1")]
|
||||
[TestCase("h264, V.MPEG4/ISO/AVC", "pd.2015.S03E08.720p.iP.WEBRip.AAC2.0.H264-BTW", "h264")]
|
||||
[TestCase("h264, V_MPEG4/ISO/AVC", "Resistance.2019.S01E03.1080p.RTE.WEB-DL.AAC2.0.x264-RTN", "x264")]
|
||||
[TestCase("wmv1, WMV1", "Droned.wmv", "WMV")]
|
||||
[TestCase("wmv2, WMV2", "Droned.wmv", "WMV")]
|
||||
[TestCase("mpeg4, XVID", "", "XviD")]
|
||||
[TestCase("mpeg4, DIV3", "spsm.dvdrip.divx.avi'.", "DivX")]
|
||||
[TestCase("vp6, 4", "Top Gear - S12E01 - Lorries - SD TV.flv", "VP6")]
|
||||
[TestCase("vp7, VP70", "Sweet Seymour.avi", "VP7")]
|
||||
[TestCase("vp8, V_VP8", "Dick.mkv", "VP8")]
|
||||
[TestCase("vp9, V_VP9", "Roadkill Ep3x11 - YouTube.webm", "VP9")]
|
||||
[TestCase("h264, x264", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")]
|
||||
[TestCase("hevc, V_MPEGH/ISO/HEVC", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")]
|
||||
[TestCase("mpeg4, mp4v-20", "", "")]
|
||||
[TestCase("mpeg4, XVID", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")]
|
||||
[TestCase("rpza, V_QUICKTIME", "Custom", "")]
|
||||
[TestCase("mpeg4, FMP4", "", "")]
|
||||
[TestCase("mpeg4, MP42", "", "")]
|
||||
[TestCase("mpeg4, mp43", "Bubble.Guppies.S01E13.480p.WEB-DL.H.264-BTN-Custom", "")]
|
||||
[TestCase("AVC", null, "x264")]
|
||||
[TestCase("AVC", "source.title.x264.720p-Sonarr", "x264")]
|
||||
[TestCase("AVC", "source.title.h264.720p-Sonarr", "h264")]
|
||||
[TestCase("V_MPEGH/ISO/HEVC", null, "x265")]
|
||||
[TestCase("V_MPEGH/ISO/HEVC", "source.title.x265.720p-Sonarr", "x265")]
|
||||
[TestCase("V_MPEGH/ISO/HEVC", "source.title.h265.720p-Sonarr", "h265")]
|
||||
[TestCase("MPEG-2 Video", null, "MPEG2")]
|
||||
public void should_format_video_codec_with_source_title_legacy(string videoCodec, string sceneName, string expectedFormat)
|
||||
{
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
VideoCodec = videoCodec
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
|
||||
}
|
||||
|
||||
[TestCase("MPEG Video, 2, Main@High, ", "Droned.S01E02.1080i.HDTV.DD5.1.MPEG2-NTb", "MPEG2")]
|
||||
[TestCase("MPEG Video, V_MPEG2, Main@High, ", "", "MPEG2")]
|
||||
[TestCase("MPEG Video, , , ", "The.Simpsons.S13E04.INTERNAL-ANiVCD.mpg", "MPEG")]
|
||||
[TestCase("VC-1, WVC1, Advanced@L4, ", "B.N.S04E18.720p.WEB-DL", "VC1")]
|
||||
[TestCase("VC-1, V_MS/VFW/FOURCC / WVC1, Advanced@L3, ", "", "VC1")]
|
||||
[TestCase("VC-1, WMV3, MP@LL, ", "It's Always Sunny S07E13 The Gang's RevengeHDTV.XviD-2HD.avi", "VC1")]
|
||||
[TestCase("V.MPEG4/ISO/AVC, V.MPEG4/ISO/AVC, , ", "pd.2015.S03E08.720p.iP.WEBRip.AAC2.0.H264-BTW", "h264")]
|
||||
[TestCase("AVC / AVC, V_MPEG4/ISO/AVC, High@L4, ", "Resistance.2019.S01E03.1080p.RTE.WEB-DL.AAC2.0.x264-RTN", "x264")]
|
||||
[TestCase("WMV1, WMV1, , ", "Droned.wmv", "WMV")]
|
||||
[TestCase("WMV2, WMV2, , ", "Droned.wmv", "WMV")]
|
||||
[TestCase("xvid, xvid, , ", "", "XviD")]
|
||||
[TestCase("div3, div3, , ", "spsm.dvdrip.divx.avi'.", "DivX")]
|
||||
[TestCase("VP6, 4, , ", "Top Gear - S12E01 - Lorries - SD TV.flv", "VP6")]
|
||||
[TestCase("VP7, VP70, General, ", "Sweet Seymour.avi", "VP7")]
|
||||
[TestCase("VP8, V_VP8, , ", "Dick.mkv", "VP8")]
|
||||
[TestCase("VP9, V_VP9, , ", "Roadkill Ep3x11 - YouTube.webm", "VP9")]
|
||||
[TestCase("x264, x264, , ", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")]
|
||||
[TestCase("V_MPEGH/ISO/HEVC, V_MPEGH/ISO/HEVC, , ", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")]
|
||||
[TestCase("MPEG-4 Visual, 20, Simple@L1, Lavc52.29.0", "Will.And.Grace.S08E14.WS.DVDrip.XviD.I.Love.L.Gay-Obfuscated", "XviD")]
|
||||
[TestCase("MPEG-4 Visual, 20, Advanced Simple@L5, XviD0046", "", "XviD")]
|
||||
[TestCase("MPEG-4 Visual, 20, , ", "", "")]
|
||||
[TestCase("MPEG-4 Visual, mp4v-20, Simple@L1, Lavc57.48.101", "", "")]
|
||||
[TestCase("mp4v, mp4v, , ", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")]
|
||||
[TestCase("V_QUICKTIME, V_QUICKTIME, , ", "Custom", "")]
|
||||
[TestCase("MPEG-4 Visual, FMP4, , ", "", "")]
|
||||
[TestCase("MPEG-4 Visual, MP42, , ", "", "")]
|
||||
[TestCase("mp43, V_MS/VFW/FOURCC / mp43, , ", "Bubble.Guppies.S01E13.480p.WEB-DL.H.264-BTN-Custom", "")]
|
||||
public void should_format_video_format(string videoFormatPack, string sceneName, string expectedFormat)
|
||||
{
|
||||
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
VideoFormat = split[0],
|
||||
VideoCodecID = split[1]
|
||||
VideoCodecID = split[1],
|
||||
VideoProfile = split[2],
|
||||
VideoCodecLibrary = split[3]
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
|
||||
}
|
||||
|
||||
[TestCase("h264, x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag
|
||||
[TestCase("hevc, x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag
|
||||
[TestCase("h264, ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown
|
||||
[TestCase("hevc, ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown
|
||||
[TestCase("h264, ", "Some.Video.S01E01", "h264")] // Default value
|
||||
[TestCase("hevc, ", "Some.Video.S01E01", "h265")] // Default value
|
||||
[TestCase("AVC, AVC, , x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag
|
||||
[TestCase("HEVC, HEVC, , x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag
|
||||
[TestCase("AVC, AVC, , ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown
|
||||
[TestCase("HEVC, HEVC, , ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown
|
||||
[TestCase("AVC, AVC, , ", "Some.Video.S01E01", "h264")] // Default value
|
||||
[TestCase("HEVC, HEVC, , ", "Some.Video.S01E01", "h265")] // Default value
|
||||
public void should_format_video_format_fallbacks(string videoFormatPack, string sceneName, string expectedFormat)
|
||||
{
|
||||
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
VideoFormat = split[0],
|
||||
VideoCodecID = split[1]
|
||||
VideoCodecID = split[1],
|
||||
VideoProfile = split[2],
|
||||
VideoCodecLibrary = split[3]
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
|
||||
}
|
||||
|
||||
[TestCase("MPEG-4 Visual, 20, , Intel(R) MPEG-4 encoder based on Intel(R) IPP 6.1 build 137.20[6.1.137.763]", "", "")]
|
||||
public void should_warn_on_unknown_video_format(string videoFormatPack, string sceneName, string expectedFormat)
|
||||
{
|
||||
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
|
||||
var mediaInfoModel = new MediaInfoModel
|
||||
{
|
||||
VideoFormat = split[0],
|
||||
VideoCodecID = split[1],
|
||||
VideoProfile = split[2],
|
||||
VideoCodecLibrary = split[3]
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Test.Common;
|
||||
@@ -8,18 +8,28 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
|
||||
[TestFixture]
|
||||
public class FormatVideoDynamicRangeFixture : TestBase
|
||||
{
|
||||
[TestCase(HdrFormat.None, "")]
|
||||
[TestCase(HdrFormat.Hlg10, "HDR")]
|
||||
[TestCase(HdrFormat.Pq10, "HDR")]
|
||||
[TestCase(HdrFormat.Hdr10, "HDR")]
|
||||
[TestCase(HdrFormat.Hdr10Plus, "HDR")]
|
||||
[TestCase(HdrFormat.DolbyVision, "HDR")]
|
||||
public void should_format_video_dynamic_range(HdrFormat format, string expectedVideoDynamicRange)
|
||||
[TestCase(8, "", "", "", "", "")]
|
||||
[TestCase(8, "BT.601 NTSC", "BT.709", "", "", "")]
|
||||
[TestCase(10, "BT.2020", "PQ", "", "", "HDR")]
|
||||
[TestCase(8, "BT.2020", "PQ", "", "", "")]
|
||||
[TestCase(10, "BT.601 NTSC", "PQ", "", "", "")]
|
||||
[TestCase(10, "BT.2020", "BT.709", "", "", "")]
|
||||
[TestCase(10, "BT.2020", "HLG", "", "", "HDR")]
|
||||
[TestCase(10, "", "", "Dolby Vision", "", "HDR")]
|
||||
[TestCase(10, "", "", "SMPTE ST 2086", "HDR10", "HDR")]
|
||||
[TestCase(8, "", "", "Dolby Vision", "", "HDR")]
|
||||
[TestCase(8, "", "", "SMPTE ST 2086", "HDR10", "HDR")]
|
||||
[TestCase(10, "BT.2020", "PQ", "Dolby Vision / SMPTE ST 2086", "Blu-ray / HDR10", "HDR")]
|
||||
public void should_format_video_dynamic_range(int bitDepth, string colourPrimaries, string transferCharacteristics, string hdrFormat, string hdrFormatCompatibility, string expectedVideoDynamicRange)
|
||||
{
|
||||
var mediaInfo = new MediaInfoModel
|
||||
{
|
||||
VideoHdrFormat = format,
|
||||
SchemaRevision = 8
|
||||
VideoBitDepth = bitDepth,
|
||||
VideoColourPrimaries = colourPrimaries,
|
||||
VideoTransferCharacteristics = transferCharacteristics,
|
||||
VideoHdrFormat = hdrFormat,
|
||||
VideoHdrFormatCompatibility = hdrFormatCompatibility,
|
||||
SchemaRevision = 7
|
||||
};
|
||||
|
||||
MediaInfoFormatter.FormatVideoDynamicRange(mediaInfo).Should().Be(expectedVideoDynamicRange);
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using FFMpegCore;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common.Categories;
|
||||
@@ -45,26 +40,33 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
|
||||
|
||||
var info = Subject.GetMediaInfo(path);
|
||||
|
||||
info.VideoFormat.Should().Be("h264");
|
||||
info.VideoCodec.Should().BeNull();
|
||||
info.VideoFormat.Should().Be("AVC");
|
||||
info.VideoCodecID.Should().Be("avc1");
|
||||
info.VideoProfile.Should().Be("Constrained Baseline");
|
||||
info.AudioFormat.Should().Be("aac");
|
||||
info.AudioCodecID.Should().Be("mp4a");
|
||||
info.AudioProfile.Should().Be("LC");
|
||||
info.AudioBitrate.Should().Be(125488);
|
||||
info.AudioChannels.Should().Be(2);
|
||||
info.AudioChannelPositions.Should().Be("stereo");
|
||||
info.AudioLanguages.Should().BeEquivalentTo("eng");
|
||||
info.VideoProfile.Should().Be("Baseline@L2.1");
|
||||
info.VideoCodecLibrary.Should().Be("");
|
||||
info.AudioFormat.Should().Be("AAC");
|
||||
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
|
||||
info.AudioProfile.Should().BeOneOf("", "LC");
|
||||
info.AudioCodecLibrary.Should().Be("");
|
||||
info.AudioBitrate.Should().Be(128000);
|
||||
info.AudioChannelsContainer.Should().Be(2);
|
||||
info.AudioChannelsStream.Should().Be(0);
|
||||
info.AudioChannelPositionsTextContainer.Should().Be("Front: L R");
|
||||
info.AudioChannelPositionsTextStream.Should().Be("");
|
||||
info.AudioLanguages.Should().Be("English");
|
||||
info.Height.Should().Be(320);
|
||||
info.RunTime.Seconds.Should().Be(10);
|
||||
info.ScanType.Should().Be("Progressive");
|
||||
info.Subtitles.Should().BeEmpty();
|
||||
info.VideoBitrate.Should().Be(193328);
|
||||
info.Subtitles.Should().Be("");
|
||||
info.VideoBitrate.Should().Be(193329);
|
||||
info.VideoFps.Should().Be(24);
|
||||
info.Width.Should().Be(480);
|
||||
info.VideoBitDepth.Should().Be(8);
|
||||
info.VideoColourPrimaries.Should().Be("smpte170m");
|
||||
info.VideoTransferCharacteristics.Should().Be("bt709");
|
||||
info.VideoColourPrimaries.Should().Be("BT.601 NTSC");
|
||||
info.VideoTransferCharacteristics.Should().Be("BT.709");
|
||||
info.AudioAdditionalFeatures.Should().BeOneOf("", "LC");
|
||||
info.VideoHdrFormat.Should().BeEmpty();
|
||||
info.VideoHdrFormatCompatibility.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -81,48 +83,45 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
|
||||
|
||||
var info = Subject.GetMediaInfo(path);
|
||||
|
||||
info.VideoFormat.Should().Be("h264");
|
||||
info.VideoCodec.Should().BeNull();
|
||||
info.VideoFormat.Should().Be("AVC");
|
||||
info.VideoCodecID.Should().Be("avc1");
|
||||
info.VideoProfile.Should().Be("Constrained Baseline");
|
||||
info.AudioFormat.Should().Be("aac");
|
||||
info.AudioCodecID.Should().Be("mp4a");
|
||||
info.AudioProfile.Should().Be("LC");
|
||||
info.AudioBitrate.Should().Be(125488);
|
||||
info.AudioChannels.Should().Be(2);
|
||||
info.AudioChannelPositions.Should().Be("stereo");
|
||||
info.AudioLanguages.Should().BeEquivalentTo("eng");
|
||||
info.VideoProfile.Should().Be("Baseline@L2.1");
|
||||
info.VideoCodecLibrary.Should().Be("");
|
||||
info.AudioFormat.Should().Be("AAC");
|
||||
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
|
||||
info.AudioProfile.Should().BeOneOf("", "LC");
|
||||
info.AudioCodecLibrary.Should().Be("");
|
||||
info.AudioBitrate.Should().Be(128000);
|
||||
info.AudioChannelsContainer.Should().Be(2);
|
||||
info.AudioChannelsStream.Should().Be(0);
|
||||
info.AudioChannelPositionsTextContainer.Should().Be("Front: L R");
|
||||
info.AudioChannelPositionsTextStream.Should().Be("");
|
||||
info.AudioLanguages.Should().Be("English");
|
||||
info.Height.Should().Be(320);
|
||||
info.RunTime.Seconds.Should().Be(10);
|
||||
info.ScanType.Should().Be("Progressive");
|
||||
info.Subtitles.Should().BeEmpty();
|
||||
info.VideoBitrate.Should().Be(193328);
|
||||
info.Subtitles.Should().Be("");
|
||||
info.VideoBitrate.Should().Be(193329);
|
||||
info.VideoFps.Should().Be(24);
|
||||
info.Width.Should().Be(480);
|
||||
info.VideoColourPrimaries.Should().Be("smpte170m");
|
||||
info.VideoTransferCharacteristics.Should().Be("bt709");
|
||||
info.VideoColourPrimaries.Should().Be("BT.601 NTSC");
|
||||
info.VideoTransferCharacteristics.Should().Be("BT.709");
|
||||
info.AudioAdditionalFeatures.Should().BeOneOf("", "LC");
|
||||
info.VideoHdrFormat.Should().BeEmpty();
|
||||
info.VideoHdrFormatCompatibility.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[TestCase(8, "", "", "", HdrFormat.None)]
|
||||
[TestCase(10, "", "", "", HdrFormat.None)]
|
||||
[TestCase(10, "bt709", "bt709", "", HdrFormat.None)]
|
||||
[TestCase(8, "bt2020", "smpte2084", "", HdrFormat.None)]
|
||||
[TestCase(10, "bt2020", "bt2020-10", "", HdrFormat.Hlg10)]
|
||||
[TestCase(10, "bt2020", "arib-std-b67", "", HdrFormat.Hlg10)]
|
||||
[TestCase(10, "bt2020", "smpte2084", "", HdrFormat.Pq10)]
|
||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", HdrFormat.Pq10)]
|
||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.MasteringDisplayMetadata", HdrFormat.Hdr10)]
|
||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.ContentLightLevelMetadata", HdrFormat.Hdr10)]
|
||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", HdrFormat.Hdr10Plus)]
|
||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", HdrFormat.DolbyVision)]
|
||||
public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, HdrFormat expected)
|
||||
[Test]
|
||||
public void should_dispose_file_after_scanning_mediainfo()
|
||||
{
|
||||
var assembly = Assembly.GetAssembly(typeof(FFProbe));
|
||||
var types = sideDataTypes.Split(",").Select(x => x.Trim()).ToList();
|
||||
var sideData = types.Where(x => x.IsNotNullOrWhiteSpace()).Select(x => assembly.CreateInstance(x)).Cast<SideData>().ToList();
|
||||
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
|
||||
|
||||
var result = VideoFileInfoReader.GetHdrFormat(bitDepth, colourPrimaries, transferFunction, sideData);
|
||||
var info = Subject.GetMediaInfo(path);
|
||||
|
||||
result.Should().Be(expected);
|
||||
var stream = new FileStream(path, FileMode.Open, FileAccess.Write);
|
||||
|
||||
stream.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
@@ -26,7 +25,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
|
||||
public void should_return_language_for_single_known_language()
|
||||
{
|
||||
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
|
||||
.With(m => m.AudioLanguages = new List<string> { "eng" })
|
||||
.With(m => m.AudioLanguages = "English")
|
||||
.Build();
|
||||
|
||||
var localMovie = Builder<LocalMovie>.CreateNew()
|
||||
@@ -43,7 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
|
||||
public void should_only_return_one_when_language_duplicated()
|
||||
{
|
||||
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
|
||||
.With(m => m.AudioLanguages = new List<string> { "eng", "eng" })
|
||||
.With(m => m.AudioLanguages = "English / English")
|
||||
.Build();
|
||||
|
||||
var localMovie = Builder<LocalMovie>.CreateNew()
|
||||
@@ -60,7 +59,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
|
||||
public void should_return_null_if_all_unknown()
|
||||
{
|
||||
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
|
||||
.With(m => m.AudioLanguages = new List<string> { "pirate", "pirate" })
|
||||
.With(m => m.AudioLanguages = "Pirate / Pirate")
|
||||
.Build();
|
||||
|
||||
var localMovie = Builder<LocalMovie>.CreateNew()
|
||||
@@ -76,7 +75,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
|
||||
public void should_return_known_languages_only()
|
||||
{
|
||||
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
|
||||
.With(m => m.AudioLanguages = new List<string> { "eng", "pirate" })
|
||||
.With(m => m.AudioLanguages = "English / Pirate")
|
||||
.Build();
|
||||
|
||||
var localMovie = Builder<LocalMovie>.CreateNew()
|
||||
@@ -93,7 +92,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
|
||||
public void should_return_multiple_known_languages()
|
||||
{
|
||||
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
|
||||
.With(m => m.AudioLanguages = new List<string> { "eng", "ger" })
|
||||
.With(m => m.AudioLanguages = "English / German")
|
||||
.Build();
|
||||
|
||||
var localMovie = Builder<LocalMovie>.CreateNew()
|
||||
|
||||
@@ -9,7 +9,7 @@ using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augmenters.Quality
|
||||
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality
|
||||
{
|
||||
[TestFixture]
|
||||
public class AugmentQualityFromReleaseNameFixture : CoreTest<AugmentQualityFromReleaseName>
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
|
||||
_fileInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "The Office" },
|
||||
MovieTitle = "The Office",
|
||||
Year = 2018,
|
||||
Quality = _quality
|
||||
};
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.AlternativeTitles;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.MovieTests.MovieServiceTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class FindByTitleFixture : CoreTest<MovieService>
|
||||
{
|
||||
private List<Movie> _candidates;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_candidates = Builder<Movie>.CreateListOfSize(3)
|
||||
.TheFirst(1)
|
||||
.With(x => x.CleanTitle = "batman")
|
||||
.With(x => x.Year = 2000)
|
||||
.TheNext(1)
|
||||
.With(x => x.CleanTitle = "batman")
|
||||
.With(x => x.Year = 1999)
|
||||
.TheRest()
|
||||
.With(x => x.CleanTitle = "darkknight")
|
||||
.With(x => x.Year = 2008)
|
||||
.With(x => x.AlternativeTitles = new List<AlternativeTitle>
|
||||
{
|
||||
new AlternativeTitle
|
||||
{
|
||||
CleanTitle = "batman"
|
||||
}
|
||||
})
|
||||
.Build()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_find_by_title_year()
|
||||
{
|
||||
var movie = Subject.FindByTitle(new List<string> { "batman" }, 2000, new List<string>(), _candidates);
|
||||
|
||||
movie.Should().NotBeNull();
|
||||
movie.Year.Should().Be(2000);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_find_candidates_by_alt_titles()
|
||||
{
|
||||
var movie = Subject.FindByTitle(new List<string> { "batman" }, 2008, new List<string>(), _candidates);
|
||||
movie.Should().NotBeNull();
|
||||
movie.Year.Should().Be(2008);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
[Test]
|
||||
public void should_add_collection_movie_if_valid_mediainfo()
|
||||
{
|
||||
GiventValidMediaInfo(Quality.Bluray1080p, "5.1", "DTS", "Progressive");
|
||||
GiventValidMediaInfo(Quality.Bluray1080p, "3/2/0.1", "DTS", "Interlaced");
|
||||
|
||||
Subject.AddMovieToCollection(_traktSettings, _downloadMessage.Movie, _downloadMessage.MovieFile);
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
|
||||
t.Movies.First().Audio == "dts" &&
|
||||
t.Movies.First().AudioChannels == "5.1" &&
|
||||
t.Movies.First().Resolution == "hd_1080p" &&
|
||||
t.Movies.First().Resolution == "hd_1080i" &&
|
||||
t.Movies.First().MediaType == "bluray"),
|
||||
It.IsAny<string>()), Times.Once());
|
||||
}
|
||||
@@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
[Test]
|
||||
public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie()
|
||||
{
|
||||
GiventValidMediaInfo(Quality.Bluray1080p, "2.0", "DTS", "Progressive");
|
||||
GiventValidMediaInfo(Quality.Bluray1080p, "2/0/0", "DTS", "Interlaced");
|
||||
|
||||
Subject.AddMovieToCollection(_traktSettings, _downloadMessage.Movie, _downloadMessage.MovieFile);
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
|
||||
t.Movies.First().Audio == "dts" &&
|
||||
t.Movies.First().AudioChannels == "2.0" &&
|
||||
t.Movies.First().Resolution == "hd_1080p" &&
|
||||
t.Movies.First().Resolution == "hd_1080i" &&
|
||||
t.Movies.First().MediaType == "bluray"),
|
||||
It.IsAny<string>()), Times.Once());
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[Platform(Exclude = "Win")]
|
||||
[TestFixture]
|
||||
|
||||
public class FileNameBuilderFixture : CoreTest<FileNameBuilder>
|
||||
@@ -369,44 +368,35 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
|
||||
_movieFile.MediaInfo = new MediaInfoModel()
|
||||
{
|
||||
VideoFormat = "h264",
|
||||
AudioFormat = "dts",
|
||||
AudioLanguages = new List<string> { "eng", "spa" },
|
||||
Subtitles = new List<string> { "eng", "spa", "ita" }
|
||||
VideoFormat = "AVC",
|
||||
AudioFormat = "DTS",
|
||||
AudioLanguages = "English/Spanish",
|
||||
Subtitles = "English/Spanish/Italian"
|
||||
};
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
.Should().Be("South.Park.H264.DTS[EN+ES].[EN+ES+IT]");
|
||||
}
|
||||
|
||||
[TestCase("nob", "NB")]
|
||||
[TestCase("swe", "SV")]
|
||||
[TestCase("zho", "ZH")]
|
||||
[TestCase("chi", "ZH")]
|
||||
[TestCase("fre", "FR")]
|
||||
[TestCase("rum", "RO")]
|
||||
[TestCase("per", "FA")]
|
||||
[TestCase("ger", "DE")]
|
||||
[TestCase("cze", "CS")]
|
||||
[TestCase("ice", "IS")]
|
||||
[TestCase("dut", "NL")]
|
||||
[TestCase("nor", "NO")]
|
||||
[TestCase("Norwegian Bokmal", "NB")]
|
||||
[TestCase("Swedis", "SV")]
|
||||
[TestCase("Chinese", "ZH")]
|
||||
public void should_format_languagecodes_properly(string language, string code)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.FULL}";
|
||||
|
||||
_movieFile.MediaInfo = new MediaInfoModel()
|
||||
{
|
||||
VideoFormat = "h264",
|
||||
AudioFormat = "dts",
|
||||
AudioChannels = 6,
|
||||
AudioLanguages = new List<string> { "eng" },
|
||||
Subtitles = new List<string> { language },
|
||||
VideoCodec = "AVC",
|
||||
AudioFormat = "DTS",
|
||||
AudioChannelsContainer = 6,
|
||||
AudioLanguages = "English",
|
||||
Subtitles = language,
|
||||
SchemaRevision = 3
|
||||
};
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
.Should().Be($"South.Park.H264.DTS.[{code}]");
|
||||
.Should().Be($"South.Park.X264.DTS.[{code}]");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -416,17 +406,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
|
||||
_movieFile.MediaInfo = new MediaInfoModel()
|
||||
{
|
||||
VideoFormat = "h264",
|
||||
AudioFormat = "dts",
|
||||
AudioLanguages = new List<string> { "eng" },
|
||||
Subtitles = new List<string> { "eng", "spa", "ita" }
|
||||
VideoFormat = "AVC",
|
||||
AudioFormat = "DTS",
|
||||
AudioLanguages = "English",
|
||||
Subtitles = "English/Spanish/Italian"
|
||||
};
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
.Should().Be("South.Park.H264.DTS.[EN+ES+IT]");
|
||||
}
|
||||
|
||||
[Ignore("not currently supported")]
|
||||
[Test]
|
||||
public void should_format_mediainfo_3d_properly()
|
||||
{
|
||||
@@ -434,11 +423,11 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
|
||||
_movieFile.MediaInfo = new MediaInfoModel()
|
||||
{
|
||||
VideoFormat = "h264",
|
||||
VideoFormat = "AVC",
|
||||
VideoMultiViewCount = 2,
|
||||
AudioFormat = "dts",
|
||||
AudioLanguages = new List<string> { "eng" },
|
||||
Subtitles = new List<string> { "eng", "spa", "ita" }
|
||||
AudioFormat = "DTS",
|
||||
AudioLanguages = "English",
|
||||
Subtitles = "English/Spanish/Italian"
|
||||
};
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
@@ -642,8 +631,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
.Should().Be(releaseGroup);
|
||||
}
|
||||
|
||||
[TestCase("eng", "")]
|
||||
[TestCase("eng/deu", "[EN+DE]")]
|
||||
[TestCase("English", "")]
|
||||
[TestCase("English/German", "[EN+DE]")]
|
||||
public void should_format_audio_languages(string audioLanguages, string expected)
|
||||
{
|
||||
_movieFile.ReleaseGroup = null;
|
||||
@@ -656,8 +645,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("eng", "[EN]")]
|
||||
[TestCase("eng/deu", "[EN+DE]")]
|
||||
[TestCase("English", "[EN]")]
|
||||
[TestCase("English/German", "[EN+DE]")]
|
||||
public void should_format_audio_languages_all(string audioLanguages, string expected)
|
||||
{
|
||||
_movieFile.ReleaseGroup = null;
|
||||
@@ -670,15 +659,19 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase(HdrFormat.None, "South.Park")]
|
||||
[TestCase(HdrFormat.Hlg10, "South.Park.HDR")]
|
||||
[TestCase(HdrFormat.Hdr10, "South.Park.HDR")]
|
||||
public void should_include_hdr_for_mediainfo_videodynamicrange_with_valid_properties(HdrFormat hdrFormat, string expectedName)
|
||||
[TestCase(8, "BT.601 NTSC", "BT.709", "South.Park")]
|
||||
[TestCase(10, "BT.2020", "PQ", "South.Park.HDR")]
|
||||
[TestCase(10, "BT.2020", "HLG", "South.Park.HDR")]
|
||||
[TestCase(0, null, null, "South.Park")]
|
||||
public void should_include_hdr_for_mediainfo_videodynamicrange_with_valid_properties(int bitDepth,
|
||||
string colourPrimaries,
|
||||
string transferCharacteristics,
|
||||
string expectedName)
|
||||
{
|
||||
_namingConfig.StandardMovieFormat =
|
||||
"{Movie.Title}.{MediaInfo VideoDynamicRange}";
|
||||
|
||||
GivenMediaInfoModel(hdrFormat: hdrFormat);
|
||||
GivenMediaInfoModel(videoBitDepth: bitDepth, videoColourPrimaries: colourPrimaries, videoTransferCharacteristics: transferCharacteristics);
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
.Should().Be(expectedName);
|
||||
@@ -750,24 +743,26 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
Mocker.GetMock<IUpdateMediaInfo>().Verify(v => v.Update(_movieFile, _movie), Times.Never());
|
||||
}
|
||||
|
||||
private void GivenMediaInfoModel(string videoCodec = "h264",
|
||||
string audioCodec = "dts",
|
||||
private void GivenMediaInfoModel(string videoCodec = "AVC",
|
||||
string audioCodec = "DTS",
|
||||
int audioChannels = 6,
|
||||
int videoBitDepth = 8,
|
||||
HdrFormat hdrFormat = HdrFormat.None,
|
||||
string audioLanguages = "eng",
|
||||
string subtitles = "eng/spa/ita",
|
||||
string videoColourPrimaries = "",
|
||||
string videoTransferCharacteristics = "",
|
||||
string audioLanguages = "English",
|
||||
string subtitles = "English/Spanish/Italian",
|
||||
int schemaRevision = 5)
|
||||
{
|
||||
_movieFile.MediaInfo = new MediaInfoModel
|
||||
{
|
||||
VideoFormat = videoCodec,
|
||||
VideoCodec = videoCodec,
|
||||
AudioFormat = audioCodec,
|
||||
AudioChannels = audioChannels,
|
||||
AudioLanguages = audioLanguages.Split("/").ToList(),
|
||||
Subtitles = subtitles.Split("/").ToList(),
|
||||
AudioChannelsContainer = audioChannels,
|
||||
AudioLanguages = audioLanguages,
|
||||
Subtitles = subtitles,
|
||||
VideoBitDepth = videoBitDepth,
|
||||
VideoHdrFormat = hdrFormat,
|
||||
VideoColourPrimaries = videoColourPrimaries,
|
||||
VideoTransferCharacteristics = videoTransferCharacteristics,
|
||||
SchemaRevision = schemaRevision
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
public void should_properly_parse_hashed_releases(string path, string title, Quality quality, string releaseGroup)
|
||||
{
|
||||
var result = Parser.Parser.ParseMoviePath(path);
|
||||
result.PrimaryMovieTitle.Should().Be(title);
|
||||
result.MovieTitle.Should().Be(title);
|
||||
result.Quality.Quality.Should().Be(quality);
|
||||
result.ReleaseGroup.Should().Be(releaseGroup);
|
||||
}
|
||||
|
||||
@@ -61,9 +61,12 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("World.Movie.Z.2.EXTENDED.2013.German.DL.1080p.BluRay.AVC-XANOR", "World Movie Z 2")]
|
||||
[TestCase("G.I.Movie.Movie.2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "G.I. Movie Movie")]
|
||||
[TestCase("www.Torrenting.org - Movie.2008.720p.X264-DIMENSION", "Movie")]
|
||||
|
||||
//Dont think this will ever apply but adding it to keep the regex in line with upstream
|
||||
[TestCase("Movie name: 2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "Movie name")]
|
||||
public void should_parse_movie_title(string postTitle, string title)
|
||||
{
|
||||
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(title);
|
||||
Parser.Parser.ParseMovieTitle(postTitle).MovieTitle.Should().Be(title);
|
||||
}
|
||||
|
||||
[TestCase("Movie.Aufbruch.nach.Pandora.Extended.2009.German.DTS.720p.BluRay.x264-SoW", "Movie Aufbruch nach Pandora", "Extended", 2009)]
|
||||
@@ -99,85 +102,16 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
ParsedMovieInfo movie = Parser.Parser.ParseMovieTitle(postTitle);
|
||||
using (new AssertionScope())
|
||||
{
|
||||
movie.PrimaryMovieTitle.Should().Be(title);
|
||||
movie.MovieTitle.Should().Be(title);
|
||||
movie.Edition.Should().Be(edition);
|
||||
movie.Year.Should().Be(year);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("L'hypothèse.du.tableau.volé.AKA.The.Hypothesis.of.the.Stolen.Painting.1978.1080p.CINET.WEB-DL.AAC2.0.x264-Cinefeel.mkv",
|
||||
new string[]
|
||||
{
|
||||
"L'hypothèse du tableau volé AKA The Hypothesis of the Stolen Painting",
|
||||
"L'hypothèse du tableau volé",
|
||||
"The Hypothesis of the Stolen Painting"
|
||||
})]
|
||||
[TestCase("Akahige.AKA.Red.Beard.1965.CD1.CRiTERiON.DVDRip.XviD-KG.avi",
|
||||
new string[]
|
||||
{
|
||||
"Akahige AKA Red Beard",
|
||||
"Akahige",
|
||||
"Red Beard"
|
||||
})]
|
||||
[TestCase("Akasen.chitai.AKA.Street.of.Shame.1956.1080p.BluRay.x264.FLAC.1.0.mkv",
|
||||
new string[]
|
||||
{
|
||||
"Akasen chitai AKA Street of Shame",
|
||||
"Akasen chitai",
|
||||
"Street of Shame"
|
||||
})]
|
||||
[TestCase("Time.Under.Fire.(aka.Beneath.the.Bermuda.Triangle).1997.DVDRip.x264.CG-Grzechsin.mkv",
|
||||
new string[]
|
||||
{
|
||||
"Time Under Fire (aka Beneath the Bermuda Triangle)",
|
||||
"Time Under Fire",
|
||||
"Beneath the Bermuda Triangle"
|
||||
})]
|
||||
[TestCase("Nochnoy.prodavet. AKA.Graveyard.Shift.2005.DVDRip.x264-HANDJOB.mkv",
|
||||
new string[]
|
||||
{
|
||||
"Nochnoy prodavet AKA Graveyard Shift",
|
||||
"Nochnoy prodavet",
|
||||
"Graveyard Shift"
|
||||
})]
|
||||
[TestCase("AKA.2002.DVDRip.x264-HANDJOB.mkv",
|
||||
new string[]
|
||||
{
|
||||
"AKA"
|
||||
})]
|
||||
[TestCase("Unbreakable.2000.BluRay.1080p.DTS.x264.dxva-EuReKA.mkv",
|
||||
new string[]
|
||||
{
|
||||
"Unbreakable"
|
||||
})]
|
||||
[TestCase("Aka Ana (2008).avi",
|
||||
new string[]
|
||||
{
|
||||
"Aka Ana"
|
||||
})]
|
||||
[TestCase("Return to Return to Nuke 'em High aka Volume 2 (2018) 1080p.mp4",
|
||||
new string[]
|
||||
{
|
||||
"Return to Return to Nuke 'em High aka Volume 2",
|
||||
"Return to Return to Nuke 'em High",
|
||||
"Volume 2"
|
||||
})]
|
||||
public void should_parse_movie_alternative_titles(string postTitle, string[] parsedTitles)
|
||||
{
|
||||
var movieInfo = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||
|
||||
movieInfo.MovieTitles.Count.Should().Be(parsedTitles.Length);
|
||||
|
||||
for (var i = 0; i < movieInfo.MovieTitles.Count; i += 1)
|
||||
{
|
||||
movieInfo.MovieTitles[i].Should().Be(parsedTitles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("(1995) Movie Name", "Movie Name")]
|
||||
public void should_parse_movie_folder_name(string postTitle, string title)
|
||||
{
|
||||
Parser.Parser.ParseMovieTitle(postTitle, true).PrimaryMovieTitle.Should().Be(title);
|
||||
Parser.Parser.ParseMovieTitle(postTitle, true).MovieTitle.Should().Be(title);
|
||||
}
|
||||
|
||||
[TestCase("1776.1979.EXTENDED.720p.BluRay.X264-AMIABLE", 1979)]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Parser.Augmenters;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -18,7 +17,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests.AugmentersTests
|
||||
{
|
||||
MovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "A Movie" },
|
||||
MovieTitle = "A Movie",
|
||||
Year = 1998,
|
||||
SimpleReleaseTitle = "A Movie Title 1998 Bluray 1080p",
|
||||
Quality = new QualityModel(Quality.Bluray1080p)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Movies;
|
||||
@@ -29,7 +28,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
Subject.GetMovie(title);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Verify(s => s.FindByTitle(Parser.Parser.ParseMovieTitle(title, false).MovieTitles, It.IsAny<int>(), It.IsAny<List<string>>(), null), Times.Once());
|
||||
.Verify(s => s.FindByTitle(Parser.Parser.ParseMovieTitle(title, false).MovieTitle, It.IsAny<int>(), null, null, null), Times.Once());
|
||||
}
|
||||
|
||||
/*[Test]
|
||||
|
||||
@@ -45,69 +45,69 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
|
||||
_parsedMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { _movie.Title },
|
||||
MovieTitle = _movie.Title,
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = _movie.Year,
|
||||
};
|
||||
|
||||
_wrongYearInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { _movie.Title },
|
||||
MovieTitle = _movie.Title,
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = 1900,
|
||||
};
|
||||
|
||||
_wrongTitleInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "Other Title" },
|
||||
MovieTitle = "Other Title",
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = 2015
|
||||
};
|
||||
|
||||
_alternativeTitleInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { _movie.AlternativeTitles.First().Title },
|
||||
MovieTitle = _movie.AlternativeTitles.First().Title,
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = _movie.Year,
|
||||
};
|
||||
|
||||
_translationTitleInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { _movie.Translations.First().Title },
|
||||
MovieTitle = _movie.Translations.First().Title,
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = _movie.Year,
|
||||
};
|
||||
|
||||
_romanTitleInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "Fack Ju Göthe II" },
|
||||
MovieTitle = "Fack Ju Göthe II",
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = _movie.Year,
|
||||
};
|
||||
|
||||
_umlautInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "Fack Ju Goethe 2" },
|
||||
MovieTitle = "Fack Ju Goethe 2",
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = _movie.Year
|
||||
};
|
||||
|
||||
_umlautAltInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "Fack Ju Goethe 2: Same same" },
|
||||
MovieTitle = "Fack Ju Goethe 2: Same same",
|
||||
Languages = new List<Language> { Language.English },
|
||||
Year = _movie.Year
|
||||
};
|
||||
|
||||
_multiLanguageInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = { _movie.Title },
|
||||
MovieTitle = _movie.Title,
|
||||
Languages = new List<Language> { Language.Original, Language.French }
|
||||
};
|
||||
|
||||
_multiLanguageWithOriginalInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = { _movie.Title },
|
||||
MovieTitle = _movie.Title,
|
||||
Languages = new List<Language> { Language.Original, Language.French, Language.English }
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
Subject.Map(_parsedMovieInfo, "", null);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Verify(v => v.FindByTitle(It.IsAny<List<string>>(), It.IsAny<int>(), It.IsAny<List<string>>(), null), Times.Once());
|
||||
.Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>(), null, null, null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -87,8 +87,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Name.S06E11.The.Santa.Simulation.480p.WEB-DL.x264-mSD", false)]
|
||||
[TestCase("Movie.Name.S02E04.480p.WEB.DL.nSD.x264-NhaNc3", false)]
|
||||
[TestCase("[HorribleSubs] Movie Title! 2018 [Web][MKV][h264][480p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
||||
[TestCase("[SubsPlease] Movie Title (540p) [AB649D32].mkv", false)]
|
||||
[TestCase("[Erai-raws] Movie Title [540p][Multiple Subtitle].mkv", false)]
|
||||
public void should_parse_webdl480p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.WEBDL, proper, Resolution.R480p);
|
||||
@@ -150,7 +148,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie Name.S01E01.The.Insanity.Principle.720p.WEB-DL.DD5.1.H.264-BD", false)]
|
||||
[TestCase("[HorribleSubs] Movie Title! 2018 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
||||
[TestCase("[HorribleSubs] Movie Title! 2018 [Web][MKV][h264][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
||||
[TestCase("Movie.Title.2013.960p.WEB-DL.AAC2.0.H.264-squalor", false)]
|
||||
public void should_parse_webdl720p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.WEBDL, proper, Resolution.R720p);
|
||||
@@ -289,7 +286,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Name.2008.BDREMUX.1080p.Bluray.AVC.DTS-HR.MA.5.1-LEGi0N")]
|
||||
[TestCase("Movie.Title.M.2008.USA.BluRay.Remux.1080p.MPEG-2.DD.5.1-TDD")]
|
||||
[TestCase("Movie.Title.2018.1080p.BluRay.REMUX.MPEG-2.DTS-HD.MA.5.1-EPSiLON")]
|
||||
[TestCase("Movie.Title.II.2003.4K.BluRay.Remux.1080p.AVC.DTS-HD.MA.5.1-BMF")]
|
||||
public void should_parse_remux1080p_quality(string title)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.BLURAY, false, Resolution.R1080p, Modifier.REMUX);
|
||||
|
||||
@@ -118,7 +118,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-EVO-4P", "EVO")]
|
||||
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-EVO-4Planet", "EVO")]
|
||||
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-DON-AlteZachen", "DON")]
|
||||
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-HarrHD-RePACKPOST", "HarrHD")]
|
||||
|
||||
public void should_not_include_bad_suffix_in_release_group(string title, string expected)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net5.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Dapper" Version="2.0.90" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Section { get; set; }
|
||||
public HiddenType Hidden { get; set; }
|
||||
public PrivacyLevel Privacy { get; set; }
|
||||
public string Placeholder { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace NzbDrone.Core.Blocklisting
|
||||
bool Blocklisted(int movieId, ReleaseInfo release);
|
||||
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
||||
List<Blocklist> GetByMovieId(int movieId);
|
||||
void Block(RemoteMovie remoteMovie, string message);
|
||||
void Delete(int id);
|
||||
void Delete(List<int> ids);
|
||||
}
|
||||
@@ -73,30 +72,6 @@ namespace NzbDrone.Core.Blocklisting
|
||||
return _blocklistRepository.BlocklistedByMovie(movieId);
|
||||
}
|
||||
|
||||
public void Block(RemoteMovie remoteMovie, string message)
|
||||
{
|
||||
var blocklist = new Blocklist
|
||||
{
|
||||
MovieId = remoteMovie.Movie.Id,
|
||||
SourceTitle = remoteMovie.Release.Title,
|
||||
Quality = remoteMovie.ParsedMovieInfo.Quality,
|
||||
Date = DateTime.UtcNow,
|
||||
PublishedDate = remoteMovie.Release.PublishDate,
|
||||
Size = remoteMovie.Release.Size,
|
||||
Indexer = remoteMovie.Release.Indexer,
|
||||
Protocol = remoteMovie.Release.DownloadProtocol,
|
||||
Message = message,
|
||||
Languages = remoteMovie.ParsedMovieInfo.Languages
|
||||
};
|
||||
|
||||
if (remoteMovie.Release is TorrentInfo torrentRelease)
|
||||
{
|
||||
blocklist.TorrentInfoHash = torrentRelease.InfoHash;
|
||||
}
|
||||
|
||||
_blocklistRepository.Insert(blocklist);
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
_blocklistRepository.Delete(id);
|
||||
|
||||
@@ -192,6 +192,13 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("WhitelistedHardcodedSubs", value); }
|
||||
}
|
||||
|
||||
public bool RemoveCompletedDownloads
|
||||
{
|
||||
get { return GetValueBoolean("RemoveCompletedDownloads", false); }
|
||||
|
||||
set { SetValue("RemoveCompletedDownloads", value); }
|
||||
}
|
||||
|
||||
public bool AutoRedownloadFailed
|
||||
{
|
||||
get { return GetValueBoolean("AutoRedownloadFailed", true); }
|
||||
@@ -199,6 +206,13 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("AutoRedownloadFailed", value); }
|
||||
}
|
||||
|
||||
public bool RemoveFailedDownloads
|
||||
{
|
||||
get { return GetValueBoolean("RemoveFailedDownloads", true); }
|
||||
|
||||
set { SetValue("RemoveFailedDownloads", value); }
|
||||
}
|
||||
|
||||
public bool CreateEmptyMovieFolders
|
||||
{
|
||||
get { return GetValueBoolean("CreateEmptyMovieFolders", false); }
|
||||
|
||||
@@ -21,8 +21,10 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
//Completed/Failed Download Handling (Download client)
|
||||
bool EnableCompletedDownloadHandling { get; set; }
|
||||
bool RemoveCompletedDownloads { get; set; }
|
||||
|
||||
bool AutoRedownloadFailed { get; set; }
|
||||
bool RemoveFailedDownloads { get; set; }
|
||||
|
||||
//Media Management
|
||||
bool AutoUnmonitorPreviouslyDownloadedMovies { get; set; }
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.CustomFormats
|
||||
|
||||
var info = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string>() { movieFile.Movie.Title },
|
||||
MovieTitle = movieFile.Movie.Title,
|
||||
SimpleReleaseTitle = sceneName.SimplifyReleaseTitle(),
|
||||
Quality = movieFile.Quality,
|
||||
Languages = movieFile.Languages,
|
||||
@@ -112,7 +112,7 @@ namespace NzbDrone.Core.CustomFormats
|
||||
|
||||
var info = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string>() { movie.Title },
|
||||
MovieTitle = movie.Title,
|
||||
SimpleReleaseTitle = parsed?.SimpleReleaseTitle ?? blocklist.SourceTitle.SimplifyReleaseTitle(),
|
||||
Quality = blocklist.Quality,
|
||||
Languages = blocklist.Languages,
|
||||
@@ -140,7 +140,7 @@ namespace NzbDrone.Core.CustomFormats
|
||||
|
||||
var info = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string>() { movie.Title },
|
||||
MovieTitle = movie.Title,
|
||||
SimpleReleaseTitle = parsed?.SimpleReleaseTitle ?? history.SourceTitle.SimplifyReleaseTitle(),
|
||||
Quality = history.Quality,
|
||||
Languages = history.Languages,
|
||||
|
||||
@@ -1,903 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(199)]
|
||||
public class mediainfo_to_ffmpeg : NzbDroneMigrationBase
|
||||
{
|
||||
private readonly JsonSerializerOptions _serializerSettings;
|
||||
|
||||
public mediainfo_to_ffmpeg()
|
||||
{
|
||||
var serializerSettings = new JsonSerializerOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
|
||||
serializerSettings.Converters.Add(new STJTimeSpanConverter());
|
||||
serializerSettings.Converters.Add(new STJUtcConverter());
|
||||
|
||||
_serializerSettings = serializerSettings;
|
||||
}
|
||||
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.WithConnection(MigrateToFfprobe);
|
||||
}
|
||||
|
||||
private void MigrateToFfprobe(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var existing = conn.Query<MediaInfoRaw>("SELECT Id, MediaInfo, SceneName FROM MovieFiles");
|
||||
|
||||
var updated = new List<MediaInfoRaw>();
|
||||
|
||||
foreach (var row in existing)
|
||||
{
|
||||
if (row.MediaInfo.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// basic parse to check schema revision
|
||||
// in case user already tested ffmpeg branch
|
||||
var mediaInfoVersion = JsonSerializer.Deserialize<MediaInfoBase>(row.MediaInfo, _serializerSettings);
|
||||
if (mediaInfoVersion.SchemaRevision >= 8)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// parse and migrate
|
||||
var mediaInfo = JsonSerializer.Deserialize<MediaInfo198>(row.MediaInfo, _serializerSettings);
|
||||
|
||||
var ffprobe = MigrateMediaInfo(mediaInfo, row.SceneName);
|
||||
|
||||
updated.Add(new MediaInfoRaw
|
||||
{
|
||||
Id = row.Id,
|
||||
MediaInfo = JsonSerializer.Serialize(ffprobe, _serializerSettings)
|
||||
});
|
||||
}
|
||||
|
||||
var updateSql = "UPDATE MovieFiles SET MediaInfo = @MediaInfo WHERE Id = @Id";
|
||||
conn.Execute(updateSql, updated, transaction: tran);
|
||||
}
|
||||
|
||||
public MediaInfo199 MigrateMediaInfo(MediaInfo198 old, string sceneName)
|
||||
{
|
||||
var m = new MediaInfo199
|
||||
{
|
||||
SchemaRevision = old.SchemaRevision,
|
||||
ContainerFormat = old.ContainerFormat,
|
||||
VideoProfile = old.VideoProfile,
|
||||
VideoBitrate = old.VideoBitrate,
|
||||
VideoBitDepth = old.VideoBitDepth,
|
||||
VideoMultiViewCount = old.VideoMultiViewCount,
|
||||
VideoColourPrimaries = MigratePrimaries(old.VideoColourPrimaries),
|
||||
VideoTransferCharacteristics = MigrateTransferCharacteristics(old.VideoTransferCharacteristics),
|
||||
Height = old.Height,
|
||||
Width = old.Width,
|
||||
AudioBitrate = old.AudioBitrate,
|
||||
RunTime = old.RunTime,
|
||||
AudioStreamCount = old.AudioStreamCount,
|
||||
VideoFps = old.VideoFps,
|
||||
ScanType = old.ScanType,
|
||||
AudioLanguages = MigrateLanguages(old.AudioLanguages),
|
||||
Subtitles = MigrateLanguages(old.Subtitles)
|
||||
};
|
||||
|
||||
m.VideoHdrFormat = MigrateHdrFormat(old);
|
||||
|
||||
MigrateVideoCodec(old, m, sceneName);
|
||||
MigrateAudioCodec(old, m);
|
||||
MigrateAudioChannelPositions(old, m);
|
||||
|
||||
m.AudioChannels = old.AudioChannelsStream > 0 ? old.AudioChannelsStream : old.AudioChannelsContainer;
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private void MigrateVideoCodec(MediaInfo198 mediaInfo, MediaInfo199 m, string sceneName)
|
||||
{
|
||||
if (mediaInfo.VideoFormat == null)
|
||||
{
|
||||
MigrateVideoCodecLegacy(mediaInfo, m, sceneName);
|
||||
return;
|
||||
}
|
||||
|
||||
var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty;
|
||||
var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty;
|
||||
|
||||
var result = mediaInfo.VideoFormat.Trim();
|
||||
|
||||
m.VideoFormat = result;
|
||||
m.VideoCodecID = null;
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("x264"))
|
||||
{
|
||||
m.VideoFormat = "h264";
|
||||
m.VideoCodecID = "x264";
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC"))
|
||||
{
|
||||
m.VideoFormat = "h264";
|
||||
|
||||
if (videoCodecLibrary.StartsWithIgnoreCase("x264"))
|
||||
{
|
||||
m.VideoCodecID = "x264";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC"))
|
||||
{
|
||||
m.VideoFormat = "hevc";
|
||||
if (videoCodecLibrary.StartsWithIgnoreCase("x265"))
|
||||
{
|
||||
m.VideoCodecID = "x265";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG Video"))
|
||||
{
|
||||
if (videoCodecID == "2" || videoCodecID == "V_MPEG2")
|
||||
{
|
||||
m.VideoFormat = "mpeg2video";
|
||||
}
|
||||
|
||||
if (videoCodecID.IsNullOrWhiteSpace())
|
||||
{
|
||||
m.VideoFormat = "MPEG";
|
||||
}
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG-2 Video"))
|
||||
{
|
||||
m.VideoFormat = "mpeg2video";
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual"))
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
|
||||
if (videoCodecID.ContainsIgnoreCase("XVID") ||
|
||||
videoCodecLibrary.StartsWithIgnoreCase("XviD"))
|
||||
{
|
||||
m.VideoCodecID = "XVID";
|
||||
}
|
||||
|
||||
if (videoCodecID.ContainsIgnoreCase("DIV3") ||
|
||||
videoCodecID.ContainsIgnoreCase("DIVX") ||
|
||||
videoCodecID.ContainsIgnoreCase("DX50") ||
|
||||
videoCodecLibrary.StartsWithIgnoreCase("DivX"))
|
||||
{
|
||||
m.VideoCodecID = "DIVX";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v"))
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
|
||||
result = GetSceneNameMatch(sceneName, "XviD", "DivX", "");
|
||||
|
||||
if (result == "XviD")
|
||||
{
|
||||
m.VideoCodecID = "XVID";
|
||||
}
|
||||
|
||||
if (result == "DivX")
|
||||
{
|
||||
m.VideoCodecID = "DIVX";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("VC-1"))
|
||||
{
|
||||
m.VideoFormat = "vc1";
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("AV1"))
|
||||
{
|
||||
m.VideoFormat = "av1";
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") ||
|
||||
videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9"))
|
||||
{
|
||||
m.VideoFormat = videoFormat.First().ToLowerInvariant();
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2"))
|
||||
{
|
||||
m.VideoFormat = "WMV";
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3"))
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
m.VideoCodecID = "DIVX";
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("XviD"))
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
m.VideoCodecID = "XVID";
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") ||
|
||||
videoFormat.ContainsIgnoreCase("RealVideo 4"))
|
||||
{
|
||||
m.VideoFormat = "qtrle";
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoFormat.ContainsIgnoreCase("mp42") ||
|
||||
videoFormat.ContainsIgnoreCase("mp43"))
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateVideoCodecLegacy(MediaInfo198 mediaInfo, MediaInfo199 m, string sceneName)
|
||||
{
|
||||
var videoCodec = mediaInfo.VideoCodec;
|
||||
|
||||
m.VideoFormat = videoCodec;
|
||||
m.VideoCodecID = null;
|
||||
|
||||
if (videoCodec.IsNullOrWhiteSpace())
|
||||
{
|
||||
m.VideoFormat = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoCodec == "AVC")
|
||||
{
|
||||
m.VideoFormat = "h264";
|
||||
}
|
||||
|
||||
if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC")
|
||||
{
|
||||
m.VideoFormat = "hevc";
|
||||
}
|
||||
|
||||
if (videoCodec == "MPEG-2 Video")
|
||||
{
|
||||
m.VideoFormat = "mpeg2video";
|
||||
}
|
||||
|
||||
if (videoCodec == "MPEG-4 Visual")
|
||||
{
|
||||
var result = GetSceneNameMatch(sceneName, "DivX", "XviD");
|
||||
if (result == "DivX")
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
m.VideoCodecID = "DIVX";
|
||||
}
|
||||
|
||||
m.VideoFormat = "mpeg4";
|
||||
m.VideoCodecID = "XVID";
|
||||
}
|
||||
|
||||
if (videoCodec.StartsWithIgnoreCase("XviD"))
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
m.VideoCodecID = "XVID";
|
||||
}
|
||||
|
||||
if (videoCodec.StartsWithIgnoreCase("DivX"))
|
||||
{
|
||||
m.VideoFormat = "mpeg4";
|
||||
m.VideoCodecID = "DIVX";
|
||||
}
|
||||
|
||||
if (videoCodec.EqualsIgnoreCase("VC-1"))
|
||||
{
|
||||
m.VideoFormat = "vc1";
|
||||
}
|
||||
}
|
||||
|
||||
private HdrFormat MigrateHdrFormat(MediaInfo198 mediaInfo)
|
||||
{
|
||||
if (mediaInfo.VideoHdrFormatCompatibility.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("HLG"))
|
||||
{
|
||||
return HdrFormat.Hlg10;
|
||||
}
|
||||
|
||||
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby"))
|
||||
{
|
||||
return HdrFormat.DolbyVision;
|
||||
}
|
||||
|
||||
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby"))
|
||||
{
|
||||
return HdrFormat.DolbyVision;
|
||||
}
|
||||
|
||||
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10+"))
|
||||
{
|
||||
return HdrFormat.Hdr10Plus;
|
||||
}
|
||||
|
||||
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10"))
|
||||
{
|
||||
return HdrFormat.Hdr10;
|
||||
}
|
||||
}
|
||||
|
||||
return VideoFileInfoReader.GetHdrFormat(mediaInfo.VideoBitDepth, mediaInfo.VideoColourPrimaries, mediaInfo.VideoTransferCharacteristics, new ());
|
||||
}
|
||||
|
||||
private void MigrateAudioCodec(MediaInfo198 mediaInfo, MediaInfo199 m)
|
||||
{
|
||||
if (mediaInfo.AudioCodecID == null)
|
||||
{
|
||||
MigrateAudioCodecLegacy(mediaInfo, m);
|
||||
return;
|
||||
}
|
||||
|
||||
var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty;
|
||||
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
m.AudioFormat = "";
|
||||
|
||||
if (audioFormat.Empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("Atmos"))
|
||||
{
|
||||
m.AudioFormat = "truehd";
|
||||
m.AudioCodecID = "thd+";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("MLP FBA"))
|
||||
{
|
||||
m.AudioFormat = "truehd";
|
||||
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch"))
|
||||
{
|
||||
m.AudioCodecID = "thd+";
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("TrueHD"))
|
||||
{
|
||||
m.AudioFormat = "truehd";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("FLAC"))
|
||||
{
|
||||
m.AudioFormat = "flac";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("DTS"))
|
||||
{
|
||||
m.AudioFormat = "dts";
|
||||
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("XLL"))
|
||||
{
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("X"))
|
||||
{
|
||||
m.AudioProfile = "DTS:X";
|
||||
return;
|
||||
}
|
||||
|
||||
m.AudioProfile = "DTS-HD MA";
|
||||
return;
|
||||
}
|
||||
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("ES"))
|
||||
{
|
||||
m.AudioProfile = "DTS-ES";
|
||||
return;
|
||||
}
|
||||
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("XBR"))
|
||||
{
|
||||
m.AudioProfile = "DTS-HD HRA";
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("E-AC-3"))
|
||||
{
|
||||
m.AudioFormat = "eac3";
|
||||
|
||||
if (splitAdditionalFeatures.ContainsIgnoreCase("JOC"))
|
||||
{
|
||||
m.AudioCodecID = "ec+3";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("AC-3"))
|
||||
{
|
||||
m.AudioFormat = "ac3";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("AAC"))
|
||||
{
|
||||
m.AudioFormat = "aac";
|
||||
|
||||
if (audioCodecID == "A_AAC/MPEG4/LC/SBR")
|
||||
{
|
||||
m.AudioCodecID = audioCodecID;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("mp3"))
|
||||
{
|
||||
m.AudioFormat = "mp3";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("MPEG Audio"))
|
||||
{
|
||||
if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3")
|
||||
{
|
||||
m.AudioFormat = "mp3";
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2")
|
||||
{
|
||||
m.AudioFormat = "mp2";
|
||||
}
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("Opus"))
|
||||
{
|
||||
m.AudioFormat = "opus";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("PCM"))
|
||||
{
|
||||
m.AudioFormat = "pcm_s16le";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("ADPCM"))
|
||||
{
|
||||
m.AudioFormat = "pcm_s16le";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("Vorbis"))
|
||||
{
|
||||
m.AudioFormat = "vorbis";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.ContainsIgnoreCase("WMA"))
|
||||
{
|
||||
m.AudioFormat = "wmav1";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateAudioCodecLegacy(MediaInfo198 mediaInfo, MediaInfo199 m)
|
||||
{
|
||||
var audioFormat = mediaInfo.AudioFormat;
|
||||
|
||||
m.AudioFormat = audioFormat;
|
||||
|
||||
if (audioFormat.IsNullOrWhiteSpace())
|
||||
{
|
||||
m.AudioFormat = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("AC-3"))
|
||||
{
|
||||
m.AudioFormat = "ac3";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
|
||||
{
|
||||
m.AudioFormat = "eac3";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("AAC"))
|
||||
{
|
||||
m.AudioFormat = "aac";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3")
|
||||
{
|
||||
m.AudioFormat = "mp3";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("DTS"))
|
||||
{
|
||||
m.AudioFormat = "DTS";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("TrueHD"))
|
||||
{
|
||||
m.AudioFormat = "truehd";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("FLAC"))
|
||||
{
|
||||
m.AudioFormat = "flac";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("Vorbis"))
|
||||
{
|
||||
m.AudioFormat = "vorbis";
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioFormat.EqualsIgnoreCase("Opus"))
|
||||
{
|
||||
m.AudioFormat = "opus";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateAudioChannelPositions(MediaInfo198 mediaInfo, MediaInfo199 m)
|
||||
{
|
||||
var audioChannels = FormatAudioChannelsFromAudioChannelPositions(mediaInfo);
|
||||
|
||||
if (audioChannels == null || audioChannels == 0.0m)
|
||||
{
|
||||
audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo);
|
||||
}
|
||||
|
||||
if (audioChannels == null || audioChannels == 0.0m)
|
||||
{
|
||||
audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo);
|
||||
}
|
||||
|
||||
audioChannels ??= 0;
|
||||
|
||||
m.AudioChannelPositions = audioChannels.ToString();
|
||||
}
|
||||
|
||||
private decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfo198 mediaInfo)
|
||||
{
|
||||
var audioChannelPositions = mediaInfo.AudioChannelPositions;
|
||||
|
||||
if (audioChannelPositions.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (audioChannelPositions.Contains("+"))
|
||||
{
|
||||
return audioChannelPositions.Split('+')
|
||||
.Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (audioChannelPositions.Contains("/"))
|
||||
{
|
||||
var channelStringList = Regex.Replace(audioChannelPositions,
|
||||
@"^\d+\sobjects",
|
||||
"",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
.Replace("Object Based / ", "")
|
||||
.Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault()
|
||||
?.Split('/');
|
||||
|
||||
var positions = default(decimal);
|
||||
|
||||
if (channelStringList == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var channel in channelStringList)
|
||||
{
|
||||
var channelSplit = channel.Split(new string[] { "." }, StringSplitOptions.None);
|
||||
|
||||
if (channelSplit.Length == 3)
|
||||
{
|
||||
positions += decimal.Parse(string.Format("{0}.{1}", channelSplit[1], channelSplit[2]), CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
positions += decimal.Parse(channel, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfo198 mediaInfo)
|
||||
{
|
||||
var audioChannelPositionsTextContainer = mediaInfo.AudioChannelPositionsTextContainer;
|
||||
var audioChannelPositionsTextStream = mediaInfo.AudioChannelPositionsTextStream;
|
||||
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
|
||||
var audioChannelsStream = mediaInfo.AudioChannelsStream;
|
||||
|
||||
//Skip if the positions texts give us nothing
|
||||
if ((audioChannelPositionsTextContainer.IsNullOrWhiteSpace() || audioChannelPositionsTextContainer == "Object Based") &&
|
||||
(audioChannelPositionsTextStream.IsNullOrWhiteSpace() || audioChannelPositionsTextStream == "Object Based"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (audioChannelsStream > 0)
|
||||
{
|
||||
return audioChannelPositionsTextStream.ContainsIgnoreCase("LFE") ? audioChannelsStream - 1 + 0.1m : audioChannelsStream;
|
||||
}
|
||||
|
||||
return audioChannelPositionsTextContainer.ContainsIgnoreCase("LFE") ? audioChannelsContainer - 1 + 0.1m : audioChannelsContainer;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private decimal? FormatAudioChannelsFromAudioChannels(MediaInfo198 mediaInfo)
|
||||
{
|
||||
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
|
||||
var audioChannelsStream = mediaInfo.AudioChannelsStream;
|
||||
|
||||
var audioFormat = (mediaInfo.AudioFormat ?? string.Empty).Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Workaround https://github.com/MediaArea/MediaInfo/issues/299 for DTS-X Audio
|
||||
if (audioFormat.ContainsIgnoreCase("DTS") &&
|
||||
splitAdditionalFeatures.ContainsIgnoreCase("XLL") &&
|
||||
splitAdditionalFeatures.ContainsIgnoreCase("X") &&
|
||||
audioChannelsContainer > 0)
|
||||
{
|
||||
return audioChannelsContainer - 1 + 0.1m;
|
||||
}
|
||||
|
||||
// FLAC 6 channels is likely 5.1
|
||||
if (audioFormat.ContainsIgnoreCase("FLAC") && audioChannelsContainer == 6)
|
||||
{
|
||||
return 5.1m;
|
||||
}
|
||||
|
||||
if (mediaInfo.SchemaRevision > 5)
|
||||
{
|
||||
return audioChannelsStream > 0 ? audioChannelsStream : audioChannelsContainer;
|
||||
}
|
||||
|
||||
if (mediaInfo.SchemaRevision >= 3)
|
||||
{
|
||||
return audioChannelsContainer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<string> MigrateLanguages(string mediaInfoLanguages)
|
||||
{
|
||||
var languages = new List<string>();
|
||||
|
||||
var tokens = mediaInfoLanguages.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
|
||||
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
for (int i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
if (tokens[i] == "Swedis")
|
||||
{
|
||||
// Probably typo in mediainfo (should be 'Swedish')
|
||||
languages.Add("swe");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tokens[i] == "Chinese" && OsInfo.IsNotWindows)
|
||||
{
|
||||
// Mono only has 'Chinese (Simplified)' & 'Chinese (Traditional)'
|
||||
languages.Add("zho");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tokens[i] == "Norwegian")
|
||||
{
|
||||
languages.Add("nor");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]);
|
||||
|
||||
if (cultureInfo != null)
|
||||
{
|
||||
languages.Add(cultureInfo.ThreeLetterISOLanguageName.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
private string MigratePrimaries(string primary)
|
||||
{
|
||||
return primary.IsNotNullOrWhiteSpace() ? primary.Replace("BT.", "bt") : primary;
|
||||
}
|
||||
|
||||
private string MigrateTransferCharacteristics(string transferCharacteristics)
|
||||
{
|
||||
if (transferCharacteristics == "PQ")
|
||||
{
|
||||
return "smpte2084";
|
||||
}
|
||||
|
||||
if (transferCharacteristics == "HLG")
|
||||
{
|
||||
return "arib-std-b67";
|
||||
}
|
||||
|
||||
return "bt709";
|
||||
}
|
||||
|
||||
private static string GetSceneNameMatch(string sceneName, params string[] tokens)
|
||||
{
|
||||
sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty;
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (sceneName.ContainsIgnoreCase(token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
// Last token is the default.
|
||||
return tokens.Last();
|
||||
}
|
||||
|
||||
public class MediaInfoRaw : ModelBase
|
||||
{
|
||||
public string MediaInfo { get; set; }
|
||||
public string SceneName { get; set; }
|
||||
}
|
||||
|
||||
public class MediaInfoBase
|
||||
{
|
||||
public int SchemaRevision { get; set; }
|
||||
}
|
||||
|
||||
public class MediaInfo198 : MediaInfoBase
|
||||
{
|
||||
public string ContainerFormat { get; set; }
|
||||
|
||||
// Deprecated according to MediaInfo
|
||||
public string VideoCodec { get; set; }
|
||||
public string VideoFormat { get; set; }
|
||||
public string VideoCodecID { get; set; }
|
||||
public string VideoProfile { get; set; }
|
||||
public string VideoCodecLibrary { get; set; }
|
||||
public int VideoBitrate { get; set; }
|
||||
public int VideoBitDepth { get; set; }
|
||||
public int VideoMultiViewCount { get; set; }
|
||||
public string VideoColourPrimaries { get; set; }
|
||||
public string VideoTransferCharacteristics { get; set; }
|
||||
public string VideoHdrFormat { get; set; }
|
||||
public string VideoHdrFormatCompatibility { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public string AudioFormat { get; set; }
|
||||
public string AudioCodecID { get; set; }
|
||||
public string AudioCodecLibrary { get; set; }
|
||||
public string AudioAdditionalFeatures { get; set; }
|
||||
public int AudioBitrate { get; set; }
|
||||
public TimeSpan RunTime { get; set; }
|
||||
public int AudioStreamCount { get; set; }
|
||||
public int AudioChannelsContainer { get; set; }
|
||||
public int AudioChannelsStream { get; set; }
|
||||
public string AudioChannelPositions { get; set; }
|
||||
public string AudioChannelPositionsTextContainer { get; set; }
|
||||
public string AudioChannelPositionsTextStream { get; set; }
|
||||
public string AudioProfile { get; set; }
|
||||
public decimal VideoFps { get; set; }
|
||||
public string AudioLanguages { get; set; }
|
||||
public string Subtitles { get; set; }
|
||||
public string ScanType { get; set; }
|
||||
}
|
||||
|
||||
public class MediaInfo199 : MediaInfoBase
|
||||
{
|
||||
public string ContainerFormat { get; set; }
|
||||
public string VideoFormat { get; set; }
|
||||
public string VideoCodecID { get; set; }
|
||||
public string VideoProfile { get; set; }
|
||||
public int VideoBitrate { get; set; }
|
||||
public int VideoBitDepth { get; set; }
|
||||
public int VideoMultiViewCount { get; set; }
|
||||
public string VideoColourPrimaries { get; set; }
|
||||
public string VideoTransferCharacteristics { get; set; }
|
||||
public HdrFormat VideoHdrFormat { get; set; }
|
||||
public int Height { get; set; }
|
||||
public int Width { get; set; }
|
||||
public string AudioFormat { get; set; }
|
||||
public string AudioCodecID { get; set; }
|
||||
public string AudioProfile { get; set; }
|
||||
public int AudioBitrate { get; set; }
|
||||
public TimeSpan RunTime { get; set; }
|
||||
public int AudioStreamCount { get; set; }
|
||||
public int AudioChannels { get; set; }
|
||||
public string AudioChannelPositions { get; set; }
|
||||
public decimal VideoFps { get; set; }
|
||||
public List<string> AudioLanguages { get; set; }
|
||||
public List<string> Subtitles { get; set; }
|
||||
public string ScanType { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(200)]
|
||||
public class cdh_per_downloadclient : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("DownloadClients")
|
||||
.AddColumn("RemoveCompletedDownloads").AsBoolean().NotNullable().WithDefaultValue(true)
|
||||
.AddColumn("RemoveFailedDownloads").AsBoolean().NotNullable().WithDefaultValue(true);
|
||||
|
||||
Execute.WithConnection(MoveRemoveSettings);
|
||||
}
|
||||
|
||||
private void MoveRemoveSettings(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var removeCompletedDownloads = false;
|
||||
var removeFailedDownloads = true;
|
||||
|
||||
using (var removeCompletedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removecompleteddownloads'"))
|
||||
{
|
||||
if ((removeCompletedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "true")
|
||||
{
|
||||
removeCompletedDownloads = true;
|
||||
}
|
||||
}
|
||||
|
||||
using (var removeFailedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removefaileddownloads'"))
|
||||
{
|
||||
if ((removeFailedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "false")
|
||||
{
|
||||
removeFailedDownloads = false;
|
||||
}
|
||||
}
|
||||
|
||||
using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation IN (\"RTorrent\", \"Flood\") THEN 0 ELSE ? END), RemoveFailedDownloads = ?"))
|
||||
{
|
||||
updateClientCmd.AddParameter(removeCompletedDownloads ? 1 : 0);
|
||||
updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0);
|
||||
updateClientCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var removeConfigCmd = conn.CreateCommand(tran, $"DELETE FROM Config WHERE Key IN ('removecompleteddownloads', 'removefaileddownloads')"))
|
||||
{
|
||||
removeConfigCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Data;
|
||||
using FluentMigrator;
|
||||
using FluentMigrator;
|
||||
using FluentMigrator.Builders.Create;
|
||||
using FluentMigrator.Builders.Create.Table;
|
||||
using FluentMigrator.Runner;
|
||||
@@ -17,15 +16,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity();
|
||||
}
|
||||
|
||||
public static IDbCommand CreateCommand(this IDbConnection conn, IDbTransaction tran, string query)
|
||||
{
|
||||
var command = conn.CreateCommand();
|
||||
command.Transaction = tran;
|
||||
command.CommandText = query;
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
public static void AddParameter(this System.Data.IDbCommand command, object value)
|
||||
{
|
||||
var parameter = command.CreateParameter();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -20,9 +20,8 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
ILogger<NzbDroneSQLiteProcessor> logger,
|
||||
IOptionsSnapshot<ProcessorOptions> options,
|
||||
IConnectionStringAccessor connectionStringAccessor,
|
||||
IServiceProvider serviceProvider,
|
||||
SQLiteQuoter quoter)
|
||||
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
||||
IServiceProvider serviceProvider)
|
||||
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user