mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-25 17:54:32 -04:00
Compare commits
76 Commits
v0.2.0.144
...
fix-orpheu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e05d86a58 | ||
|
|
4f83116413 | ||
|
|
d440bc079f | ||
|
|
a5c7c6cbcb | ||
|
|
710c3d6deb | ||
|
|
1959efbd09 | ||
|
|
c4468b9cbb | ||
|
|
8762d94dda | ||
|
|
e07ea80977 | ||
|
|
37c393a659 | ||
|
|
b4b779df5c | ||
|
|
1dbf35deb5 | ||
|
|
6ab226c43a | ||
|
|
3a896fc43e | ||
|
|
a81d632878 | ||
|
|
cb8c0d4aa7 | ||
|
|
788a5a3e24 | ||
|
|
4f056bf228 | ||
|
|
6ce9c779c1 | ||
|
|
32fc2ec365 | ||
|
|
9ddd38a334 | ||
|
|
b45e5a5e38 | ||
|
|
135efe6d94 | ||
|
|
69de6d18eb | ||
|
|
627da14a32 | ||
|
|
83bf3620c0 | ||
|
|
fa41bf3c58 | ||
|
|
31e7a101ef | ||
|
|
22f9e9e6b7 | ||
|
|
8834431ba6 | ||
|
|
01cc9b3d07 | ||
|
|
a0cbe1de5d | ||
|
|
75792c0760 | ||
|
|
83ca724120 | ||
|
|
aee6ee1a00 | ||
|
|
e66c69a292 | ||
|
|
930370729b | ||
|
|
2f5b55013a | ||
|
|
cb74fade18 | ||
|
|
e0da422b0e | ||
|
|
77caaa2a55 | ||
|
|
55d03967ec | ||
|
|
684c30893a | ||
|
|
3b51a3a618 | ||
|
|
09bd8137fc | ||
|
|
495daf7967 | ||
|
|
bb3821c254 | ||
|
|
855f8d35f2 | ||
|
|
15dab381af | ||
|
|
9c5a88e2e7 | ||
|
|
80d295cce5 | ||
|
|
76afb70b01 | ||
|
|
c29fba3a2b | ||
|
|
68e41f0860 | ||
|
|
4c68645175 | ||
|
|
aaef8fb29c | ||
|
|
b2e300b6da | ||
|
|
77c840b03a | ||
|
|
98c3408909 | ||
|
|
40f6c2e59d | ||
|
|
64c9bb4231 | ||
|
|
94ef3ea88f | ||
|
|
dab4500b16 | ||
|
|
d951943c67 | ||
|
|
bba6f9349b | ||
|
|
7e0f88ad7a | ||
|
|
88c6cbf943 | ||
|
|
69d31f96de | ||
|
|
a5718ad937 | ||
|
|
505d9c151d | ||
|
|
a637677ec4 | ||
|
|
a0d5421dc8 | ||
|
|
95f62be50c | ||
|
|
d7b5100e35 | ||
|
|
64c1e1fa54 | ||
|
|
74663ea077 |
@@ -13,7 +13,9 @@ variables:
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.100'
|
||||
dotnetVersion: '6.0.201'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
|
||||
trigger:
|
||||
@@ -155,7 +157,7 @@ stages:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -200,16 +202,11 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x86
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create x86 .NET Core Windows installer
|
||||
- bash: |
|
||||
distribution/windows/setup/inno/ISCC.exe distribution/windows/setup/prowlarr.iss //DFramework=net6.0 //DRuntime=win-x64
|
||||
cp distribution/windows/setup/output/Prowlarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create x64 .NET Core Windows installer
|
||||
./build.sh --packages --installer
|
||||
cp distribution/windows/setup/output/Prowlarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp distribution/windows/setup/output/Prowlarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Prowlarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create Installers
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
displayName: Publish Installer
|
||||
@@ -706,17 +703,17 @@ stages:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
|
||||
failBuild: false
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: false
|
||||
failBuild: true
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
pattern: 'Prowlarr.*.windows-core-x64.zip'
|
||||
failBuild: false
|
||||
failBuild: true
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
@@ -807,7 +804,7 @@ stages:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -944,7 +941,7 @@ stages:
|
||||
- job:
|
||||
displayName: Discord Notification
|
||||
pool:
|
||||
vmImage: 'windows-2019'
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
continueOnError: true
|
||||
|
||||
39
build.sh
39
build.sh
@@ -234,6 +234,32 @@ Package()
|
||||
esac
|
||||
}
|
||||
|
||||
BuildInstaller()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe distribution/windows/setup/prowlarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
{
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
ProgressEnd "Installed portable Inno Setup"
|
||||
}
|
||||
|
||||
RemoveInno()
|
||||
{
|
||||
rm -rf _inno
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
@@ -265,6 +291,7 @@ if [ $# -eq 0 ]; then
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_BSD=NO
|
||||
fi
|
||||
@@ -300,6 +327,10 @@ case $key in
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--installer)
|
||||
INSTALLER=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
@@ -383,3 +414,11 @@ then
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>prowlarr.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.osx.prowlarr.video</string>
|
||||
<string>com.osx.prowlarr.com</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#SET BUILD_NUMBER=1
|
||||
#SET branch=develop
|
||||
inno\ISCC.exe prowlarr.iss
|
||||
@@ -1,336 +0,0 @@
|
||||
; *** Inno Setup version 5.5.3+ English messages ***
|
||||
;
|
||||
; To download user-contributed translations of this file, go to:
|
||||
; http://www.jrsoftware.org/files/istrans/
|
||||
;
|
||||
; Note: When translating this text, do not add periods (.) to the end of
|
||||
; messages that didn't have them already, because on those messages Inno
|
||||
; Setup adds the periods automatically (appending a period would result in
|
||||
; two periods being displayed).
|
||||
|
||||
[LangOptions]
|
||||
; The following three entries are very important. Be sure to read and
|
||||
; understand the '[LangOptions] section' topic in the help file.
|
||||
LanguageName=English
|
||||
LanguageID=$0409
|
||||
LanguageCodePage=0
|
||||
; If the language you are translating to requires special font faces or
|
||||
; sizes, uncomment any of the following entries and change them accordingly.
|
||||
;DialogFontName=
|
||||
;DialogFontSize=8
|
||||
;WelcomeFontName=Verdana
|
||||
;WelcomeFontSize=12
|
||||
;TitleFontName=Arial
|
||||
;TitleFontSize=29
|
||||
;CopyrightFontName=Arial
|
||||
;CopyrightFontSize=8
|
||||
|
||||
[Messages]
|
||||
|
||||
; *** Application titles
|
||||
SetupAppTitle=Setup
|
||||
SetupWindowTitle=Setup - %1
|
||||
UninstallAppTitle=Uninstall
|
||||
UninstallAppFullTitle=%1 Uninstall
|
||||
|
||||
; *** Misc. common
|
||||
InformationTitle=Information
|
||||
ConfirmTitle=Confirm
|
||||
ErrorTitle=Error
|
||||
|
||||
; *** SetupLdr messages
|
||||
SetupLdrStartupMessage=This will install %1. Do you wish to continue?
|
||||
LdrCannotCreateTemp=Unable to create a temporary file. Setup aborted
|
||||
LdrCannotExecTemp=Unable to execute file in the temporary directory. Setup aborted
|
||||
|
||||
; *** Startup error messages
|
||||
LastErrorMessage=%1.%n%nError %2: %3
|
||||
SetupFileMissing=The file %1 is missing from the installation directory. Please correct the problem or obtain a new copy of the program.
|
||||
SetupFileCorrupt=The setup files are corrupted. Please obtain a new copy of the program.
|
||||
SetupFileCorruptOrWrongVer=The setup files are corrupted, or are incompatible with this version of Setup. Please correct the problem or obtain a new copy of the program.
|
||||
InvalidParameter=An invalid parameter was passed on the command line:%n%n%1
|
||||
SetupAlreadyRunning=Setup is already running.
|
||||
WindowsVersionNotSupported=This program does not support the version of Windows your computer is running.
|
||||
WindowsServicePackRequired=This program requires %1 Service Pack %2 or later.
|
||||
NotOnThisPlatform=This program will not run on %1.
|
||||
OnlyOnThisPlatform=This program must be run on %1.
|
||||
OnlyOnTheseArchitectures=This program can only be installed on versions of Windows designed for the following processor architectures:%n%n%1
|
||||
MissingWOW64APIs=The version of Windows you are running does not include functionality required by Setup to perform a 64-bit installation. To correct this problem, please install Service Pack %1.
|
||||
WinVersionTooLowError=This program requires %1 version %2 or later.
|
||||
WinVersionTooHighError=This program cannot be installed on %1 version %2 or later.
|
||||
AdminPrivilegesRequired=You must be logged in as an administrator when installing this program.
|
||||
PowerUserPrivilegesRequired=You must be logged in as an administrator or as a member of the Power Users group when installing this program.
|
||||
SetupAppRunningError=Setup has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit.
|
||||
UninstallAppRunningError=Uninstall has detected that %1 is currently running.%n%nPlease close all instances of it now, then click OK to continue, or Cancel to exit.
|
||||
|
||||
; *** Misc. errors
|
||||
ErrorCreatingDir=Setup was unable to create the directory "%1"
|
||||
ErrorTooManyFilesInDir=Unable to create a file in the directory "%1" because it contains too many files
|
||||
|
||||
; *** Setup common messages
|
||||
ExitSetupTitle=Exit Setup
|
||||
ExitSetupMessage=Setup is not complete. If you exit now, the program will not be installed.%n%nYou may run Setup again at another time to complete the installation.%n%nExit Setup?
|
||||
AboutSetupMenuItem=&About Setup...
|
||||
AboutSetupTitle=About Setup
|
||||
AboutSetupMessage=%1 version %2%n%3%n%n%1 home page:%n%4
|
||||
AboutSetupNote=
|
||||
TranslatorNote=
|
||||
|
||||
; *** Buttons
|
||||
ButtonBack=< &Back
|
||||
ButtonNext=&Next >
|
||||
ButtonInstall=&Install
|
||||
ButtonOK=OK
|
||||
ButtonCancel=Cancel
|
||||
ButtonYes=&Yes
|
||||
ButtonYesToAll=Yes to &All
|
||||
ButtonNo=&No
|
||||
ButtonNoToAll=N&o to All
|
||||
ButtonFinish=&Finish
|
||||
ButtonBrowse=&Browse...
|
||||
ButtonWizardBrowse=B&rowse...
|
||||
ButtonNewFolder=&Make New Folder
|
||||
|
||||
; *** "Select Language" dialog messages
|
||||
SelectLanguageTitle=Select Setup Language
|
||||
SelectLanguageLabel=Select the language to use during the installation:
|
||||
|
||||
; *** Common wizard text
|
||||
ClickNext=Click Next to continue, or Cancel to exit Setup.
|
||||
BeveledLabel=
|
||||
BrowseDialogTitle=Browse For Folder
|
||||
BrowseDialogLabel=Select a folder in the list below, then click OK.
|
||||
NewFolderName=New Folder
|
||||
|
||||
; *** "Welcome" wizard page
|
||||
WelcomeLabel1=Welcome to the [name] Setup Wizard
|
||||
WelcomeLabel2=This will install [name/ver] on your computer.%n%nIt is recommended that you close all other applications before continuing.
|
||||
|
||||
; *** "Password" wizard page
|
||||
WizardPassword=Password
|
||||
PasswordLabel1=This installation is password protected.
|
||||
PasswordLabel3=Please provide the password, then click Next to continue. Passwords are case-sensitive.
|
||||
PasswordEditLabel=&Password:
|
||||
IncorrectPassword=The password you entered is not correct. Please try again.
|
||||
|
||||
; *** "License Agreement" wizard page
|
||||
WizardLicense=License Agreement
|
||||
LicenseLabel=Please read the following important information before continuing.
|
||||
LicenseLabel3=Please read the following License Agreement. You must accept the terms of this agreement before continuing with the installation.
|
||||
LicenseAccepted=I &accept the agreement
|
||||
LicenseNotAccepted=I &do not accept the agreement
|
||||
|
||||
; *** "Information" wizard pages
|
||||
WizardInfoBefore=Information
|
||||
InfoBeforeLabel=Please read the following important information before continuing.
|
||||
InfoBeforeClickLabel=When you are ready to continue with Setup, click Next.
|
||||
WizardInfoAfter=Information
|
||||
InfoAfterLabel=Please read the following important information before continuing.
|
||||
InfoAfterClickLabel=When you are ready to continue with Setup, click Next.
|
||||
|
||||
; *** "User Information" wizard page
|
||||
WizardUserInfo=User Information
|
||||
UserInfoDesc=Please enter your information.
|
||||
UserInfoName=&User Name:
|
||||
UserInfoOrg=&Organization:
|
||||
UserInfoSerial=&Serial Number:
|
||||
UserInfoNameRequired=You must enter a name.
|
||||
|
||||
; *** "Select Destination Location" wizard page
|
||||
WizardSelectDir=Select Destination Location
|
||||
SelectDirDesc=Where should [name] be installed?
|
||||
SelectDirLabel3=Setup will install [name] into the following folder.
|
||||
SelectDirBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse.
|
||||
DiskSpaceMBLabel=At least [mb] MB of free disk space is required.
|
||||
CannotInstallToNetworkDrive=Setup cannot install to a network drive.
|
||||
CannotInstallToUNCPath=Setup cannot install to a UNC path.
|
||||
InvalidPath=You must enter a full path with drive letter; for example:%n%nC:\APP%n%nor a UNC path in the form:%n%n\\server\share
|
||||
InvalidDrive=The drive or UNC share you selected does not exist or is not accessible. Please select another.
|
||||
DiskSpaceWarningTitle=Not Enough Disk Space
|
||||
DiskSpaceWarning=Setup requires at least %1 KB of free space to install, but the selected drive only has %2 KB available.%n%nDo you want to continue anyway?
|
||||
DirNameTooLong=The folder name or path is too long.
|
||||
InvalidDirName=The folder name is not valid.
|
||||
BadDirName32=Folder names cannot include any of the following characters:%n%n%1
|
||||
DirExistsTitle=Folder Exists
|
||||
DirExists=The folder:%n%n%1%n%nalready exists. Would you like to install to that folder anyway?
|
||||
DirDoesntExistTitle=Folder Does Not Exist
|
||||
DirDoesntExist=The folder:%n%n%1%n%ndoes not exist. Would you like the folder to be created?
|
||||
|
||||
; *** "Select Components" wizard page
|
||||
WizardSelectComponents=Select Components
|
||||
SelectComponentsDesc=Which components should be installed?
|
||||
SelectComponentsLabel2=Select the components you want to install; clear the components you do not want to install. Click Next when you are ready to continue.
|
||||
FullInstallation=Full installation
|
||||
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
|
||||
CompactInstallation=Compact installation
|
||||
CustomInstallation=Custom installation
|
||||
NoUninstallWarningTitle=Components Exist
|
||||
NoUninstallWarning=Setup has detected that the following components are already installed on your computer:%n%n%1%n%nDeselecting these components will not uninstall them.%n%nWould you like to continue anyway?
|
||||
ComponentSize1=%1 KB
|
||||
ComponentSize2=%1 MB
|
||||
ComponentsDiskSpaceMBLabel=Current selection requires at least [mb] MB of disk space.
|
||||
|
||||
; *** "Select Additional Tasks" wizard page
|
||||
WizardSelectTasks=Select Additional Tasks
|
||||
SelectTasksDesc=Which additional tasks should be performed?
|
||||
SelectTasksLabel2=Select the additional tasks you would like Setup to perform while installing [name], then click Next.
|
||||
|
||||
; *** "Select Start Menu Folder" wizard page
|
||||
WizardSelectProgramGroup=Select Start Menu Folder
|
||||
SelectStartMenuFolderDesc=Where should Setup place the program's shortcuts?
|
||||
SelectStartMenuFolderLabel3=Setup will create the program's shortcuts in the following Start Menu folder.
|
||||
SelectStartMenuFolderBrowseLabel=To continue, click Next. If you would like to select a different folder, click Browse.
|
||||
MustEnterGroupName=You must enter a folder name.
|
||||
GroupNameTooLong=The folder name or path is too long.
|
||||
InvalidGroupName=The folder name is not valid.
|
||||
BadGroupName=The folder name cannot include any of the following characters:%n%n%1
|
||||
NoProgramGroupCheck2=&Don't create a Start Menu folder
|
||||
|
||||
; *** "Ready to Install" wizard page
|
||||
WizardReady=Ready to Install
|
||||
ReadyLabel1=Setup is now ready to begin installing [name] on your computer.
|
||||
ReadyLabel2a=Click Install to continue with the installation, or click Back if you want to review or change any settings.
|
||||
ReadyLabel2b=Click Install to continue with the installation.
|
||||
ReadyMemoUserInfo=User information:
|
||||
ReadyMemoDir=Destination location:
|
||||
ReadyMemoType=Setup type:
|
||||
ReadyMemoComponents=Selected components:
|
||||
ReadyMemoGroup=Start Menu folder:
|
||||
ReadyMemoTasks=Additional tasks:
|
||||
|
||||
; *** "Preparing to Install" wizard page
|
||||
WizardPreparing=Preparing to Install
|
||||
PreparingDesc=Setup is preparing to install [name] on your computer.
|
||||
PreviousInstallNotCompleted=The installation/removal of a previous program was not completed. You will need to restart your computer to complete that installation.%n%nAfter restarting your computer, run Setup again to complete the installation of [name].
|
||||
CannotContinue=Setup cannot continue. Please click Cancel to exit.
|
||||
ApplicationsFound=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications.
|
||||
ApplicationsFound2=The following applications are using files that need to be updated by Setup. It is recommended that you allow Setup to automatically close these applications. After the installation has completed, Setup will attempt to restart the applications.
|
||||
CloseApplications=&Automatically close the applications
|
||||
DontCloseApplications=&Do not close the applications
|
||||
ErrorCloseApplications=Setup was unable to automatically close all applications. It is recommended that you close all applications using files that need to be updated by Setup before continuing.
|
||||
|
||||
; *** "Installing" wizard page
|
||||
WizardInstalling=Installing
|
||||
InstallingLabel=Please wait while Setup installs [name] on your computer.
|
||||
|
||||
; *** "Setup Completed" wizard page
|
||||
FinishedHeadingLabel=Completing the [name] Setup Wizard
|
||||
FinishedLabelNoIcons=Setup has finished installing [name] on your computer.
|
||||
FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed icons.
|
||||
ClickFinish=Click Finish to exit Setup.
|
||||
FinishedRestartLabel=To complete the installation of [name], Setup must restart your computer. Would you like to restart now?
|
||||
FinishedRestartMessage=To complete the installation of [name], Setup must restart your computer.%n%nWould you like to restart now?
|
||||
ShowReadmeCheck=Yes, I would like to view the README file
|
||||
YesRadio=&Yes, restart the computer now
|
||||
NoRadio=&No, I will restart the computer later
|
||||
; used for example as 'Run MyProg.exe'
|
||||
RunEntryExec=Run %1
|
||||
; used for example as 'View Readme.txt'
|
||||
RunEntryShellExec=View %1
|
||||
|
||||
; *** "Setup Needs the Next Disk" stuff
|
||||
ChangeDiskTitle=Setup Needs the Next Disk
|
||||
SelectDiskLabel2=Please insert Disk %1 and click OK.%n%nIf the files on this disk can be found in a folder other than the one displayed below, enter the correct path or click Browse.
|
||||
PathLabel=&Path:
|
||||
FileNotInDir2=The file "%1" could not be located in "%2". Please insert the correct disk or select another folder.
|
||||
SelectDirectoryLabel=Please specify the location of the next disk.
|
||||
|
||||
; *** Installation phase messages
|
||||
SetupAborted=Setup was not completed.%n%nPlease correct the problem and run Setup again.
|
||||
EntryAbortRetryIgnore=Click Retry to try again, Ignore to proceed anyway, or Abort to cancel installation.
|
||||
|
||||
; *** Installation status messages
|
||||
StatusClosingApplications=Closing applications...
|
||||
StatusCreateDirs=Creating directories...
|
||||
StatusExtractFiles=Extracting files...
|
||||
StatusCreateIcons=Creating shortcuts...
|
||||
StatusCreateIniEntries=Creating INI entries...
|
||||
StatusCreateRegistryEntries=Creating registry entries...
|
||||
StatusRegisterFiles=Registering files...
|
||||
StatusSavingUninstall=Saving uninstall information...
|
||||
StatusRunProgram=Finishing installation...
|
||||
StatusRestartingApplications=Restarting applications...
|
||||
StatusRollback=Rolling back changes...
|
||||
|
||||
; *** Misc. errors
|
||||
ErrorInternal2=Internal error: %1
|
||||
ErrorFunctionFailedNoCode=%1 failed
|
||||
ErrorFunctionFailed=%1 failed; code %2
|
||||
ErrorFunctionFailedWithMessage=%1 failed; code %2.%n%3
|
||||
ErrorExecutingProgram=Unable to execute file:%n%1
|
||||
|
||||
; *** Registry errors
|
||||
ErrorRegOpenKey=Error opening registry key:%n%1\%2
|
||||
ErrorRegCreateKey=Error creating registry key:%n%1\%2
|
||||
ErrorRegWriteKey=Error writing to registry key:%n%1\%2
|
||||
|
||||
; *** INI errors
|
||||
ErrorIniEntry=Error creating INI entry in file "%1".
|
||||
|
||||
; *** File copying errors
|
||||
FileAbortRetryIgnore=Click Retry to try again, Ignore to skip this file (not recommended), or Abort to cancel installation.
|
||||
FileAbortRetryIgnore2=Click Retry to try again, Ignore to proceed anyway (not recommended), or Abort to cancel installation.
|
||||
SourceIsCorrupted=The source file is corrupted
|
||||
SourceDoesntExist=The source file "%1" does not exist
|
||||
ExistingFileReadOnly=The existing file is marked as read-only.%n%nClick Retry to remove the read-only attribute and try again, Ignore to skip this file, or Abort to cancel installation.
|
||||
ErrorReadingExistingDest=An error occurred while trying to read the existing file:
|
||||
FileExists=The file already exists.%n%nWould you like Setup to overwrite it?
|
||||
ExistingFileNewer=The existing file is newer than the one Setup is trying to install. It is recommended that you keep the existing file.%n%nDo you want to keep the existing file?
|
||||
ErrorChangingAttr=An error occurred while trying to change the attributes of the existing file:
|
||||
ErrorCreatingTemp=An error occurred while trying to create a file in the destination directory:
|
||||
ErrorReadingSource=An error occurred while trying to read the source file:
|
||||
ErrorCopying=An error occurred while trying to copy a file:
|
||||
ErrorReplacingExistingFile=An error occurred while trying to replace the existing file:
|
||||
ErrorRestartReplace=RestartReplace failed:
|
||||
ErrorRenamingTemp=An error occurred while trying to rename a file in the destination directory:
|
||||
ErrorRegisterServer=Unable to register the DLL/OCX: %1
|
||||
ErrorRegSvr32Failed=RegSvr32 failed with exit code %1
|
||||
ErrorRegisterTypeLib=Unable to register the type library: %1
|
||||
|
||||
; *** Post-installation errors
|
||||
ErrorOpeningReadme=An error occurred while trying to open the README file.
|
||||
ErrorRestartingComputer=Setup was unable to restart the computer. Please do this manually.
|
||||
|
||||
; *** Uninstaller messages
|
||||
UninstallNotFound=File "%1" does not exist. Cannot uninstall.
|
||||
UninstallOpenError=File "%1" could not be opened. Cannot uninstall
|
||||
UninstallUnsupportedVer=The uninstall log file "%1" is in a format not recognized by this version of the uninstaller. Cannot uninstall
|
||||
UninstallUnknownEntry=An unknown entry (%1) was encountered in the uninstall log
|
||||
ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components?
|
||||
UninstallOnlyOnWin64=This installation can only be uninstalled on 64-bit Windows.
|
||||
OnlyAdminCanUninstall=This installation can only be uninstalled by a user with administrative privileges.
|
||||
UninstallStatusLabel=Please wait while %1 is removed from your computer.
|
||||
UninstalledAll=%1 was successfully removed from your computer.
|
||||
UninstalledMost=%1 uninstall complete.%n%nSome elements could not be removed. These can be removed manually.
|
||||
UninstalledAndNeedsRestart=To complete the uninstallation of %1, your computer must be restarted.%n%nWould you like to restart now?
|
||||
UninstallDataCorrupted="%1" file is corrupted. Cannot uninstall
|
||||
|
||||
; *** Uninstallation phase messages
|
||||
ConfirmDeleteSharedFileTitle=Remove Shared File?
|
||||
ConfirmDeleteSharedFile2=The system indicates that the following shared file is no longer in use by any programs. Would you like for Uninstall to remove this shared file?%n%nIf any programs are still using this file and it is removed, those programs may not function properly. If you are unsure, choose No. Leaving the file on your system will not cause any harm.
|
||||
SharedFileNameLabel=File name:
|
||||
SharedFileLocationLabel=Location:
|
||||
WizardUninstalling=Uninstall Status
|
||||
StatusUninstalling=Uninstalling %1...
|
||||
|
||||
; *** Shutdown block reasons
|
||||
ShutdownBlockReasonInstallingApp=Installing %1.
|
||||
ShutdownBlockReasonUninstallingApp=Uninstalling %1.
|
||||
|
||||
; The custom messages below aren't used by Setup itself, but if you make
|
||||
; use of them in your scripts, you'll want to translate them.
|
||||
|
||||
[CustomMessages]
|
||||
|
||||
NameAndVersion=%1 version %2
|
||||
AdditionalIcons=Additional icons:
|
||||
CreateDesktopIcon=Create a &desktop icon
|
||||
CreateQuickLaunchIcon=Create a &Quick Launch icon
|
||||
ProgramOnTheWeb=%1 on the Web
|
||||
UninstallProgram=Uninstall %1
|
||||
LaunchProgram=Launch %1
|
||||
AssocFileExtension=&Associate %1 with the %2 file extension
|
||||
AssocingFileExtension=Associating %1 with the %2 file extension...
|
||||
AutoStartProgramGroupDescription=Startup:
|
||||
AutoStartProgram=Automatically start %1
|
||||
AddonHostProgramNotFound=%1 could not be located in the folder you selected.%n%nDo you want to continue anyway?
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
@@ -3,13 +3,12 @@
|
||||
|
||||
#define AppName "Prowlarr"
|
||||
#define AppPublisher "Team Prowlarr"
|
||||
#define AppURL "https://prowlarr.video/"
|
||||
#define ForumsURL "https://forums.prowlarr.video/"
|
||||
#define AppURL "https://prowlarr.com/"
|
||||
#define ForumsURL "https://prowlarr.com/discord/"
|
||||
#define AppExeName "Prowlarr.exe"
|
||||
#define BaseVersion GetEnv('MAJORVERSION')
|
||||
#define BuildNumber GetEnv('MINORVERSION')
|
||||
#define BuildVersion GetEnv('PROWLARRVERSION')
|
||||
#define BranchName GetEnv('BUILD_SOURCEBRANCHNAME')
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
@@ -22,15 +21,15 @@ AppPublisher={#AppPublisher}
|
||||
AppPublisherURL={#AppURL}
|
||||
AppSupportURL={#ForumsURL}
|
||||
AppUpdatesURL={#AppURL}
|
||||
DefaultDirName={commonappdata}\Prowlarr\bin
|
||||
DefaultDirName={commonappdata}\Prowlarr
|
||||
DisableDirPage=yes
|
||||
DefaultGroupName={#AppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename=Prowlarr.{#BranchName}.{#BuildVersion}.windows.{#Framework}
|
||||
OutputBaseFilename=Prowlarr.{#BuildVersion}.{#Runtime}
|
||||
SolidCompression=yes
|
||||
AppCopyright=Creative Commons 3.0 License
|
||||
AllowUNCPath=False
|
||||
UninstallDisplayIcon={app}\Prowlarr.exe
|
||||
UninstallDisplayIcon={app}\bin\Prowlarr.exe
|
||||
DisableReadyPage=True
|
||||
CompressionThreads=2
|
||||
Compression=lzma2/normal
|
||||
@@ -38,6 +37,7 @@ AppContact={#ForumsURL}
|
||||
VersionInfoVersion={#BaseVersion}.{#BuildNumber}
|
||||
SetupLogging=yes
|
||||
OutputDir=output
|
||||
WizardStyle=modern
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
@@ -48,28 +48,31 @@ Name: "windowsService"; Description: "Install Windows Service (Starts when the c
|
||||
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
|
||||
[Dirs]
|
||||
Name: "{app}"; Permissions: users-modify
|
||||
|
||||
[Files]
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\Prowlarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\*"; Excludes: "Prowlarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\Prowlarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Prowlarr\*"; Excludes: "Prowlarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
|
||||
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
|
||||
Name: "{userstartup}\{#AppName}"; Filename: "{app}\Prowlarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\bin\{#AppExeName}"; Parameters: "/icon"
|
||||
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\bin\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
|
||||
Name: "{userstartup}\{#AppName}"; Filename: "{app}\bin\Prowlarr.exe"; WorkingDir: "{app}\bin"; Tasks: startupShortcut
|
||||
|
||||
[InstallDelete]
|
||||
Name: "{app}"; Type: filesandordirs
|
||||
Name: "{app}\bin"; Type: filesandordirs
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\Prowlarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
|
||||
Filename: "{app}\Prowlarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
|
||||
Filename: "{app}\Prowlarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
|
||||
Filename: "{app}\Prowlarr.exe"; Description: "Open Prowlarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
|
||||
Filename: "{app}\Prowlarr.exe"; Description: "Start Prowlarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
|
||||
Filename: "{app}\bin\Prowlarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
|
||||
Filename: "{app}\bin\Prowlarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
|
||||
Filename: "{app}\bin\Prowlarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
|
||||
Filename: "{app}\bin\Prowlarr.exe"; Description: "Open Prowlarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
|
||||
Filename: "{app}\bin\Prowlarr.exe"; Description: "Start Prowlarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\prowlarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
|
||||
Filename: "{app}\bin\prowlarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
|
||||
|
||||
[Code]
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
|
||||
4
docs.sh
4
docs.sh
@@ -27,11 +27,11 @@ dotnet clean $slnFile -c Release
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.2.3 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
|
||||
|
||||
sleep 10
|
||||
sleep 30
|
||||
|
||||
kill %1
|
||||
|
||||
|
||||
@@ -47,10 +47,6 @@ class Link extends Component {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else if (to.startsWith(`${window.Prowlarr.urlBase}/`)) {
|
||||
el = RouterLink;
|
||||
linkProps.to = to;
|
||||
linkProps.target = target;
|
||||
} else {
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
|
||||
@@ -37,7 +37,8 @@ function ModalError(props) {
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>);
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
ModalError.propTypes = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
import { Scrollbars } from 'react-custom-scrollbars-2';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import styles from './OverlayScroller.css';
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ const columns = [
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: translate('Description'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
label: translate('Privacy'),
|
||||
@@ -136,12 +142,12 @@ class AddIndexerModalContent extends Component {
|
||||
return true;
|
||||
});
|
||||
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load indexers');
|
||||
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add Indexer
|
||||
{translate('AddIndexer')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
|
||||
@@ -32,6 +32,7 @@ class SelectIndexerRow extends Component {
|
||||
privacy,
|
||||
name,
|
||||
language,
|
||||
description,
|
||||
isExistingIndexer
|
||||
} = this.props;
|
||||
|
||||
@@ -61,6 +62,10 @@ class SelectIndexerRow extends Component {
|
||||
{language}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{description}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{translate(firstCharToUpper(privacy))}
|
||||
</TableRowCell>
|
||||
@@ -74,6 +79,7 @@ SelectIndexerRow.propTypes = {
|
||||
protocol: PropTypes.string.isRequired,
|
||||
privacy: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
isExistingIndexer: PropTypes.bool.isRequired
|
||||
|
||||
@@ -4,6 +4,7 @@ import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
@@ -20,6 +21,7 @@ function IndexerInfoModalContent(props) {
|
||||
language,
|
||||
indexerUrls,
|
||||
protocol,
|
||||
capabilities,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
@@ -30,41 +32,78 @@ function IndexerInfoModalContent(props) {
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Id')}
|
||||
data={id}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Description')}
|
||||
data={description ? description : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Encoding')}
|
||||
data={encoding ? encoding : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Language')}
|
||||
data={language ?? '-'}
|
||||
/>
|
||||
|
||||
<DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
</DescriptionList>
|
||||
<FieldSet legend={translate('IndexerDetails')}>
|
||||
<div className={styles.groups}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Id')}
|
||||
data={id}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Description')}
|
||||
data={description ? description : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Encoding')}
|
||||
data={encoding ? encoding : '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Language')}
|
||||
data={language ?? '-'}
|
||||
/>
|
||||
<DescriptionListItemTitle>{translate('IndexerSite')}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
|
||||
</DescriptionListItemDescription>
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</FieldSet>
|
||||
<FieldSet legend={translate('SearchCapabilities')}>
|
||||
<div className={styles.groups}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('RawSearchSupported')}
|
||||
data={capabilities.supportsRawSearch ? translate('Yes') : translate('No')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('SearchTypes')}
|
||||
data={capabilities.searchParams.length === 0 ? translate('NotSupported') : capabilities.searchParams[0]}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('TVSearchTypes')}
|
||||
data={capabilities.tvSearchParams.length === 0 ? translate('NotSupported') : capabilities.tvSearchParams.join(', ')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MovieSearchTypes')}
|
||||
data={capabilities.movieSearchParams.length === 0 ? translate('NotSupported') : capabilities.movieSearchParams.join(', ')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('BookSearchTypes')}
|
||||
data={capabilities.bookSearchParams.length === 0 ? translate('NotSupported') : capabilities.bookSearchParams.join(', ')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MusicSearchTypes')}
|
||||
data={capabilities.musicSearchParams.length === 0 ? translate('NotSupported') : capabilities.musicSearchParams.join(', ')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</FieldSet>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</ModalContent >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,6 +115,7 @@ IndexerInfoModalContent.propTypes = {
|
||||
language: PropTypes.string.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
capabilities: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ class QueryParameterModal extends Component {
|
||||
onSelectionChange={this.onInputSelectionChange}
|
||||
/>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -37,7 +37,10 @@ class SearchFooter extends Component {
|
||||
searchingReleases: false,
|
||||
searchQuery: defaultSearchQuery || '',
|
||||
searchIndexerIds: defaultIndexerIds,
|
||||
searchCategories: defaultCategories
|
||||
searchCategories: defaultCategories,
|
||||
searchLimit: 100,
|
||||
searchOffset: 0,
|
||||
newSearch: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,11 +118,28 @@ class SearchFooter extends Component {
|
||||
};
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories, this.state.searchType);
|
||||
|
||||
const {
|
||||
searchLimit,
|
||||
searchOffset,
|
||||
searchQuery,
|
||||
searchIndexerIds,
|
||||
searchCategories,
|
||||
searchType
|
||||
} = this.state;
|
||||
|
||||
this.props.onSearchPress(searchQuery, searchIndexerIds, searchCategories, searchType, searchLimit, searchOffset);
|
||||
|
||||
this.setState({ searchOffset: searchOffset + 100, newSearch: false });
|
||||
};
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
this.setState({ searchQuery: value });
|
||||
this.setState({ searchQuery: value, newSearch: true, searchOffset: 0 });
|
||||
};
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onInputChange({ name, value });
|
||||
this.setState({ newSearch: true, searchOffset: 0 });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -141,6 +161,7 @@ class SearchFooter extends Component {
|
||||
searchQuery,
|
||||
searchIndexerIds,
|
||||
searchCategories,
|
||||
newSearch,
|
||||
isQueryParameterModalOpen,
|
||||
queryModalOptions,
|
||||
searchType
|
||||
@@ -206,7 +227,7 @@ class SearchFooter extends Component {
|
||||
name='searchIndexerIds'
|
||||
value={searchIndexerIds}
|
||||
isDisabled={isFetching}
|
||||
onChange={onInputChange}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -220,7 +241,7 @@ class SearchFooter extends Component {
|
||||
name='searchCategories'
|
||||
value={searchCategories}
|
||||
isDisabled={isFetching}
|
||||
onChange={onInputChange}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -243,7 +264,7 @@ class SearchFooter extends Component {
|
||||
isDisabled={isFetching || !hasIndexers || selectedCount === 0}
|
||||
onPress={onBulkGrabPress}
|
||||
>
|
||||
{translate('Grab Releases')}
|
||||
{translate('GrabReleases')}
|
||||
</SpinnerButton>
|
||||
}
|
||||
|
||||
@@ -253,7 +274,7 @@ class SearchFooter extends Component {
|
||||
isDisabled={isFetching || !hasIndexers}
|
||||
onPress={this.onSearchPress}
|
||||
>
|
||||
{translate('Search')}
|
||||
{newSearch ? translate('Search') : translate('More')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -196,8 +196,8 @@ class SearchIndex extends Component {
|
||||
this.setState({ jumpToCharacter });
|
||||
};
|
||||
|
||||
onSearchPress = (query, indexerIds, categories, type) => {
|
||||
this.props.onSearchPress({ query, indexerIds, categories, type });
|
||||
onSearchPress = (query, indexerIds, categories, type, limit, offset) => {
|
||||
this.props.onSearchPress({ query, indexerIds, categories, type, limit, offset });
|
||||
};
|
||||
|
||||
onBulkGrabPress = () => {
|
||||
|
||||
@@ -5,13 +5,14 @@ import createAllIndexersSelector from './createAllIndexersSelector';
|
||||
function createProfileInUseSelector(profileProp) {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.appProfiles.items,
|
||||
createAllIndexersSelector(),
|
||||
(id, indexers) => {
|
||||
(id, profiles, indexers) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.some(indexers, { [profileProp]: id })) {
|
||||
if (_.some(indexers, { [profileProp]: id }) || profiles.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
82
package.json
82
package.json
@@ -25,17 +25,17 @@
|
||||
"not chrome < 60"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "5.15.3",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||
"@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",
|
||||
"@sentry/browser": "6.15.0",
|
||||
"@sentry/integrations": "6.15.0",
|
||||
"chart.js": "3.2.0",
|
||||
"@fortawesome/fontawesome-free": "6.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"@microsoft/signalr": "6.0.3",
|
||||
"@sentry/browser": "6.19.2",
|
||||
"@sentry/integrations": "6.19.2",
|
||||
"chart.js": "3.7.1",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.8",
|
||||
"clipboard": "2.0.10",
|
||||
"connected-react-router": "6.9.1",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "6.3.0",
|
||||
@@ -48,13 +48,13 @@
|
||||
"moment": "2.29.1",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.10.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.10.3",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.1.0",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-custom-scrollbars-2": "4.4.0",
|
||||
"react-dnd": "14.0.4",
|
||||
"react-dnd-html5-backend": "14.0.2",
|
||||
"react-dnd-multi-backend": "6.0.2",
|
||||
@@ -78,41 +78,41 @@
|
||||
"reselect": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.0",
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.0",
|
||||
"@babel/plugin-proposal-decorators": "7.16.4",
|
||||
"@babel/plugin-proposal-export-default-from": "7.16.0",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.16.0",
|
||||
"@babel/plugin-proposal-function-sent": "7.16.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.0",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.16.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.16.0",
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/eslint-parser": "7.17.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-decorators": "7.17.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.16.7",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
|
||||
"@babel/plugin-proposal-function-sent": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.16.4",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"autoprefixer": "10.2.5",
|
||||
"babel-loader": "8.2.3",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"autoprefixer": "10.4.4",
|
||||
"babel-loader": "8.2.4",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.11.0",
|
||||
"css-loader": "6.5.1",
|
||||
"eslint": "8.3.0",
|
||||
"core-js": "3.21.1",
|
||||
"css-loader": "6.7.1",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.25.3",
|
||||
"eslint-plugin-react": "7.27.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-react": "7.29.4",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "3.1.0",
|
||||
"esprint": "3.3.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "6.1.7",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"loader-utils": "^3.0.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"postcss": "8.3.11",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"postcss": "8.4.12",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "6.2.0",
|
||||
"postcss-mixins": "8.1.0",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-mixins": "9.0.2",
|
||||
"postcss-nested": "5.0.6",
|
||||
"postcss-simple-vars": "6.0.3",
|
||||
"postcss-url": "10.1.3",
|
||||
@@ -121,11 +121,11 @@
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.1.0",
|
||||
"stylelint": "14.6.0",
|
||||
"stylelint-order": "5.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.64.2",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,10 +94,10 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Automation.Test
|
||||
public abstract class AutomationTest
|
||||
{
|
||||
private NzbDroneRunner _runner;
|
||||
protected RemoteWebDriver driver;
|
||||
protected WebDriver driver;
|
||||
|
||||
public AutomationTest()
|
||||
{
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Remote;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
|
||||
namespace NzbDrone.Automation.Test.PageModel
|
||||
{
|
||||
public class PageBase
|
||||
{
|
||||
private readonly RemoteWebDriver _driver;
|
||||
private readonly WebDriver _driver;
|
||||
|
||||
public PageBase(RemoteWebDriver driver)
|
||||
public PageBase(WebDriver driver)
|
||||
{
|
||||
_driver = driver;
|
||||
driver.Manage().Window.Maximize();
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="90.0.4430.2400" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="99.0.4844.5100" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
|
||||
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"https://torrentseeds.org/api/torrents/filter?api_token=2b51db35e1912ffc138825a12b9933d2&name=&sortField=created_at&sortDirection=desc&perPage=100&page=1")]
|
||||
|
||||
//Indexer Responses
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
|
||||
"/boot",
|
||||
"/lib",
|
||||
"/sbin",
|
||||
"/proc"
|
||||
"/proc",
|
||||
"/usr/bin"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
do
|
||||
{
|
||||
request.Url += new HttpUri(response.Headers.GetSingleValue("Location"));
|
||||
request.Url = new HttpUri(response.RedirectUrl);
|
||||
autoRedirectChain.Add(request.Url.ToString());
|
||||
|
||||
_logger.Trace("Redirected to {0}", request.Url);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
public class HttpResponse
|
||||
{
|
||||
private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
|
||||
private static readonly Regex RegexRefresh = new Regex("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled);
|
||||
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
@@ -67,7 +67,8 @@ namespace NzbDrone.Common.Http
|
||||
StatusCode == HttpStatusCode.MovedPermanently ||
|
||||
StatusCode == HttpStatusCode.RedirectMethod ||
|
||||
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
||||
StatusCode == HttpStatusCode.Found;
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
Headers.ContainsKey("Refresh");
|
||||
|
||||
public string RedirectUrl
|
||||
{
|
||||
@@ -76,6 +77,20 @@ namespace NzbDrone.Common.Http
|
||||
var newUrl = Headers["Location"];
|
||||
if (newUrl == null)
|
||||
{
|
||||
newUrl = Headers["Refresh"];
|
||||
|
||||
if (newUrl == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var match = RegexRefresh.Match(newUrl);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return (Request.Url += new HttpUri(match.Groups[2].Value)).FullUri;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=[?&: ;])(apikey|(?:access[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.9" />
|
||||
<PackageReference Include="Sentry" Version="3.8.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
<PackageReference Include="Sentry" Version="3.15.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Applications;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class CleanupOrphanedApplicationFixture : DbTest<CleanupOrphanedApplicationStatus, ApplicationStatus>
|
||||
{
|
||||
private ApplicationDefinition _application;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_application = Builder<ApplicationDefinition>.CreateNew()
|
||||
.BuildNew();
|
||||
}
|
||||
|
||||
private void GivenApplication()
|
||||
{
|
||||
Db.Insert(_application);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_orphaned_applicationstatus()
|
||||
{
|
||||
var status = Builder<ApplicationStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _application.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_applicationstatus()
|
||||
{
|
||||
GivenApplication();
|
||||
|
||||
var status = Builder<ApplicationStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _application.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
AllStoredModels.Should().Contain(h => h.ProviderId == _application.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients.Flood;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class CleanupOrphanedDownloadClientStatusFixture : DbTest<CleanupOrphanedDownloadClientStatus, DownloadClientStatus>
|
||||
{
|
||||
private DownloadClientDefinition _downloadClient;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_downloadClient = Builder<DownloadClientDefinition>.CreateNew()
|
||||
.With(c => c.Settings = new FloodSettings())
|
||||
.BuildNew();
|
||||
}
|
||||
|
||||
private void GivenClient()
|
||||
{
|
||||
Db.Insert(_downloadClient);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_orphaned_downloadclientstatus()
|
||||
{
|
||||
var status = Builder<DownloadClientStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _downloadClient.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_downloadclientstatus()
|
||||
{
|
||||
GivenClient();
|
||||
|
||||
var status = Builder<DownloadClientStatus>.CreateNew()
|
||||
.With(h => h.ProviderId = _downloadClient.Id)
|
||||
.BuildNew();
|
||||
Db.Insert(status);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
AllStoredModels.Should().Contain(h => h.ProviderId == _downloadClient.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Applications;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.ThingiProvider.Status;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class FixFutureApplicationStatusTimesFixture : CoreTest<FixFutureApplicationStatusTimes>
|
||||
{
|
||||
[Test]
|
||||
public void should_set_disabled_till_when_its_too_far_in_the_future()
|
||||
{
|
||||
var disabledTillTime = EscalationBackOff.Periods[1];
|
||||
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(applicationStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.All(
|
||||
s => s.DisabledTill.Value <= DateTime.UtcNow.AddMinutes(disabledTillTime)))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_initial_failure_when_its_in_the_future()
|
||||
{
|
||||
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(applicationStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.All(
|
||||
s => s.InitialFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_most_recent_failure_when_its_in_the_future()
|
||||
{
|
||||
var applicationStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(applicationStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.All(
|
||||
s => s.MostRecentFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_change_statuses_when_times_are_in_the_past()
|
||||
{
|
||||
var indexerStatuses = Builder<ApplicationStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 0)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(indexerStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IApplicationStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<ApplicationStatus>>(i => i.Count == 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.ThingiProvider.Status;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class FixFutureDownloadClientStatusTimesFixture : CoreTest<FixFutureDownloadClientStatusTimes>
|
||||
{
|
||||
[Test]
|
||||
public void should_set_disabled_till_when_its_too_far_in_the_future()
|
||||
{
|
||||
var disabledTillTime = EscalationBackOff.Periods[1];
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.All(
|
||||
s => s.DisabledTill.Value <= DateTime.UtcNow.AddMinutes(disabledTillTime)))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_initial_failure_when_its_in_the_future()
|
||||
{
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.All(
|
||||
s => s.InitialFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_most_recent_failure_when_its_in_the_future()
|
||||
{
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5))
|
||||
.With(t => t.EscalationLevel = 1)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.All(
|
||||
s => s.MostRecentFailure.Value <= DateTime.UtcNow))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_change_statuses_when_times_are_in_the_past()
|
||||
{
|
||||
var clientStatuses = Builder<DownloadClientStatus>.CreateListOfSize(5)
|
||||
.All()
|
||||
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
|
||||
.With(t => t.EscalationLevel = 0)
|
||||
.BuildListOfNew();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(clientStatuses);
|
||||
|
||||
Subject.Clean();
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusRepository>()
|
||||
.Verify(v => v.UpdateMany(
|
||||
It.Is<List<DownloadClientStatus>>(i => i.Count == 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.90" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
|
||||
@@ -116,6 +116,12 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Lidarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
|
||||
{
|
||||
_logger.Error(ex, "Lidarr returned redirect and is invalid");
|
||||
return new ValidationFailure("BaseUrl", "Lidarr url is invalid, Prowlarr cannot connect to Lidarr - are you missing a url base?");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
|
||||
@@ -116,6 +116,12 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Radarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
|
||||
{
|
||||
_logger.Error(ex, "Radarr returned redirect and is invalid");
|
||||
return new ValidationFailure("BaseUrl", "Radarr url is invalid, Prowlarr cannot connect to Radarr - are you missing a url base?");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
|
||||
@@ -116,6 +116,12 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Readarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
|
||||
{
|
||||
_logger.Error(ex, "Readarr returned redirect and is invalid");
|
||||
return new ValidationFailure("BaseUrl", "Readarr url is invalid, Prowlarr cannot connect to Readarr - are you missing a url base?");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
|
||||
@@ -116,6 +116,18 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Sonarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
|
||||
{
|
||||
_logger.Error(ex, "Sonarr returned redirect and is invalid");
|
||||
return new ValidationFailure("BaseUrl", "Sonarr url is invalid, Prowlarr cannot connect to Sonarr - are you missing a url base?");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.Error(ex, "Sonarr not found");
|
||||
return new ValidationFailure("BaseUrl", "Sonarr url is invalid, Prowlarr cannot connect to Sonarr. Is Sonarr running and accessible? Sonarr v2 is not supported.");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
|
||||
179
src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs
Normal file
179
src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
public class Whisparr : ApplicationBase<WhisparrSettings>
|
||||
{
|
||||
public override string Name => "Whisparr";
|
||||
|
||||
private readonly IWhisparrV3Proxy _whisparrV3Proxy;
|
||||
private readonly ICached<List<WhisparrIndexer>> _schemaCache;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_schemaCache = cacheManager.GetCache<List<WhisparrIndexer>>(GetType());
|
||||
_whisparrV3Proxy = whisparrV3Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
var testIndexer = new IndexerDefinition
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Test",
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Capabilities = new IndexerCapabilities()
|
||||
};
|
||||
|
||||
foreach (var cat in NewznabStandardCategory.AllCats)
|
||||
{
|
||||
testIndexer.Capabilities.Categories.AddCategoryMapping(1, cat);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Whisparr"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
public override List<AppIndexerMap> GetIndexerMappings()
|
||||
{
|
||||
var indexers = _whisparrV3Proxy.GetIndexers(Settings)
|
||||
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
|
||||
|
||||
var mappings = new List<AppIndexerMap>();
|
||||
|
||||
foreach (var indexer in indexers)
|
||||
{
|
||||
if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey)
|
||||
{
|
||||
var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value);
|
||||
|
||||
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
|
||||
{
|
||||
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
|
||||
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
var radarrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
|
||||
}
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
{
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
|
||||
|
||||
if (indexerMapping != null)
|
||||
{
|
||||
//Remove Indexer remotely and then remove the mapping
|
||||
_whisparrV3Proxy.RemoveIndexer(indexerMapping.RemoteIndexerId, Settings);
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
|
||||
var radarrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
|
||||
|
||||
var remoteIndexer = _whisparrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
|
||||
|
||||
if (remoteIndexer != null)
|
||||
{
|
||||
_logger.Debug("Remote indexer found, syncing with current settings");
|
||||
|
||||
if (!radarrIndexer.Equals(remoteIndexer))
|
||||
{
|
||||
_whisparrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} to Whisparr", indexer.Name);
|
||||
radarrIndexer.Id = 0;
|
||||
var newRemoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = newRemoteIndexer.Id });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Remote indexer not found for {0}, skipping re-add to Radarr due to indexer capabilities", indexer.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
|
||||
{
|
||||
var cacheKey = $"{Settings.BaseUrl}";
|
||||
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
|
||||
|
||||
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
|
||||
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
|
||||
|
||||
var schema = protocol == DownloadProtocol.Usenet ? newznab : torznab;
|
||||
|
||||
var whisparrIndexer = new WhisparrIndexer
|
||||
{
|
||||
Id = id,
|
||||
Name = $"{indexer.Name} (Prowlarr)",
|
||||
EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss,
|
||||
EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch,
|
||||
EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch,
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
Fields = schema.Fields,
|
||||
};
|
||||
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
|
||||
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
|
||||
|
||||
return whisparrIndexer;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/NzbDrone.Core/Applications/Whisparr/WhisparrField.cs
Normal file
22
src/NzbDrone.Core/Applications/Whisparr/WhisparrField.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
public class WhisparrField
|
||||
{
|
||||
public int Order { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Unit { get; set; }
|
||||
public string HelpText { get; set; }
|
||||
public string HelpLink { get; set; }
|
||||
public object Value { get; set; }
|
||||
public string Type { get; set; }
|
||||
public bool Advanced { get; set; }
|
||||
public string Section { get; set; }
|
||||
public string Hidden { get; set; }
|
||||
|
||||
public WhisparrField Clone()
|
||||
{
|
||||
return (WhisparrField)MemberwiseClone();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs
Normal file
44
src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
public class WhisparrIndexer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool EnableRss { get; set; }
|
||||
public bool EnableAutomaticSearch { get; set; }
|
||||
public bool EnableInteractiveSearch { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string ImplementationName { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<WhisparrField> Fields { get; set; }
|
||||
|
||||
public bool Equals(WhisparrIndexer other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
|
||||
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
|
||||
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
|
||||
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
|
||||
|
||||
return other.EnableRss == EnableRss &&
|
||||
other.EnableAutomaticSearch == EnableAutomaticSearch &&
|
||||
other.EnableInteractiveSearch == EnableInteractiveSearch &&
|
||||
other.Name == Name &&
|
||||
other.Implementation == Implementation &&
|
||||
other.Priority == Priority &&
|
||||
other.Id == Id &&
|
||||
apiKey && apiPath && baseUrl && cats;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs
Normal file
46
src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
public class WhisparrSettingsValidator : AbstractValidator<WhisparrSettings>
|
||||
{
|
||||
public WhisparrSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).IsValidUrl();
|
||||
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
RuleFor(c => c.SyncCategories).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class WhisparrSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly WhisparrSettingsValidator Validator = new WhisparrSettingsValidator();
|
||||
|
||||
public WhisparrSettings()
|
||||
{
|
||||
SyncCategories = new[] { 6000, 6010, 6020, 6030, 6040, 6045, 6050, 6070, 6080, 6090 };
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Whisparr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Whisparr Server", HelpText = "URL used to connect to Whisparr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:6969")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Whisparr in Settings/General")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
public class WhisparrStatus
|
||||
{
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
163
src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs
Normal file
163
src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Whisparr
|
||||
{
|
||||
public interface IWhisparrV3Proxy
|
||||
{
|
||||
WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings);
|
||||
List<WhisparrIndexer> GetIndexers(WhisparrSettings settings);
|
||||
WhisparrIndexer GetIndexer(int indexerId, WhisparrSettings settings);
|
||||
List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings);
|
||||
void RemoveIndexer(int indexerId, WhisparrSettings settings);
|
||||
WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings);
|
||||
ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings);
|
||||
}
|
||||
|
||||
public class WhisparrV3Proxy : IWhisparrV3Proxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public WhisparrV3Proxy(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public WhisparrStatus GetStatus(WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
|
||||
return Execute<WhisparrStatus>(request);
|
||||
}
|
||||
|
||||
public List<WhisparrIndexer> GetIndexers(WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
|
||||
return Execute<List<WhisparrIndexer>>(request);
|
||||
}
|
||||
|
||||
public WhisparrIndexer GetIndexer(int indexerId, WhisparrSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
|
||||
return Execute<WhisparrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode != HttpStatusCode.NotFound)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveIndexer(int indexerId, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
|
||||
_httpClient.Execute(request);
|
||||
}
|
||||
|
||||
public List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
|
||||
return Execute<List<WhisparrIndexer>>(request);
|
||||
}
|
||||
|
||||
public WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<WhisparrIndexer>(request);
|
||||
}
|
||||
|
||||
public WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
return Execute<WhisparrIndexer>(request);
|
||||
}
|
||||
|
||||
public ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
try
|
||||
{
|
||||
Execute<WhisparrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.Error(ex, "API Key is invalid");
|
||||
return new ValidationFailure("ApiKey", "API Key is invalid");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Prowlarr URL is invalid");
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Whisparr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.SeeOther)
|
||||
{
|
||||
_logger.Error(ex, "Whisparr returned redirect and is invalid");
|
||||
return new ValidationFailure("BaseUrl", "Whisparr url is invalid, Prowlarr cannot connect to Whisparr - are you missing a url base?");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("", "Unable to send test message");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(WhisparrSettings settings, string resource, HttpMethod method)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
var request = new HttpRequestBuilder(baseUrl).Resource(resource)
|
||||
.SetHeader("X-Api-Key", settings.ApiKey)
|
||||
.Build();
|
||||
|
||||
request.Headers.ContentType = "application/json";
|
||||
|
||||
request.Method = method;
|
||||
request.AllowAutoRedirect = true;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private TResource Execute<TResource>(HttpRequest request)
|
||||
where TResource : new()
|
||||
{
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,8 @@ namespace NzbDrone.Core.Backup
|
||||
|
||||
_archiveService.CreateZip(backupPath, _diskProvider.GetFiles(_backupTempFolder, SearchOption.TopDirectoryOnly));
|
||||
|
||||
Cleanup();
|
||||
|
||||
_logger.ProgressDebug("Backup zip created");
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Datastore
|
||||
_updateSql = GetUpdateSql(_properties);
|
||||
}
|
||||
|
||||
protected virtual SqlBuilder Builder() => new SqlBuilder();
|
||||
protected virtual SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType);
|
||||
|
||||
protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList();
|
||||
|
||||
|
||||
19
src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs
Normal file
19
src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class DapperTimeSpanConverter : SqlMapper.TypeHandler<TimeSpan>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, TimeSpan value)
|
||||
{
|
||||
parameter.Value = value.ToString();
|
||||
}
|
||||
|
||||
public override TimeSpan Parse(object value)
|
||||
{
|
||||
return TimeSpan.Parse((string)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,21 +42,21 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public static SqlBuilder Where<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
|
||||
{
|
||||
var wb = new WhereBuilder(filter, true, builder.Sequence);
|
||||
var wb = GetWhereBuilder(builder.DatabaseType, filter, true, builder.Sequence);
|
||||
|
||||
return builder.Where(wb.ToString(), wb.Parameters);
|
||||
}
|
||||
|
||||
public static SqlBuilder OrWhere<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
|
||||
{
|
||||
var wb = new WhereBuilder(filter, true, builder.Sequence);
|
||||
var wb = GetWhereBuilder(builder.DatabaseType, filter, true, builder.Sequence);
|
||||
|
||||
return builder.OrWhere(wb.ToString(), wb.Parameters);
|
||||
}
|
||||
|
||||
public static SqlBuilder Join<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter)
|
||||
{
|
||||
var wb = new WhereBuilder(filter, false, builder.Sequence);
|
||||
var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence);
|
||||
|
||||
var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight));
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public static SqlBuilder LeftJoin<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter)
|
||||
{
|
||||
var wb = new WhereBuilder(filter, false, builder.Sequence);
|
||||
var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence);
|
||||
|
||||
var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight));
|
||||
|
||||
@@ -138,6 +138,18 @@ namespace NzbDrone.Core.Datastore
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static WhereBuilder GetWhereBuilder(DatabaseType databaseType, Expression filter, bool requireConcrete, int seq)
|
||||
{
|
||||
if (databaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
return new WhereBuilderPostgres(filter, requireConcrete, seq);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new WhereBuilderSqlite(filter, requireConcrete, seq);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> ToDictionary(this DynamicParameters dynamicParams)
|
||||
{
|
||||
var argsDictionary = new Dictionary<string, object>();
|
||||
|
||||
@@ -8,9 +8,17 @@ namespace NzbDrone.Core.Datastore
|
||||
public class SqlBuilder
|
||||
{
|
||||
private readonly Dictionary<string, Clauses> _data = new Dictionary<string, Clauses>();
|
||||
private readonly DatabaseType _databaseType;
|
||||
|
||||
public SqlBuilder(DatabaseType databaseType)
|
||||
{
|
||||
_databaseType = databaseType;
|
||||
}
|
||||
|
||||
public int Sequence { get; private set; }
|
||||
|
||||
public DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public Template AddTemplate(string sql, dynamic parameters = null) =>
|
||||
new Template(this, sql, parameters);
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ namespace NzbDrone.Core.Datastore
|
||||
(db, parent) =>
|
||||
{
|
||||
var id = childIdSelector(parent);
|
||||
return db.Query<TChild>(new SqlBuilder().Where<TChild>(x => x.Id == id)).SingleOrDefault();
|
||||
return db.Query<TChild>(new SqlBuilder(db.DatabaseType).Where<TChild>(x => x.Id == id)).SingleOrDefault();
|
||||
},
|
||||
parent => childIdSelector(parent) > 0);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
SqlMapper.RemoveTypeMap(typeof(DateTime));
|
||||
SqlMapper.AddTypeHandler(new DapperUtcConverter());
|
||||
SqlMapper.AddTypeHandler(new DapperTimeSpanConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>());
|
||||
SqlMapper.AddTypeHandler(new CookieConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
|
||||
|
||||
@@ -1,389 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class WhereBuilder : ExpressionVisitor
|
||||
public abstract class WhereBuilder : ExpressionVisitor
|
||||
{
|
||||
protected StringBuilder _sb;
|
||||
|
||||
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||
private readonly string _paramNamePrefix;
|
||||
private readonly bool _requireConcreteValue = false;
|
||||
private int _paramCount = 0;
|
||||
private bool _gotConcreteValue = false;
|
||||
|
||||
public WhereBuilder(Expression filter, bool requireConcreteValue, int seq)
|
||||
{
|
||||
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
|
||||
_requireConcreteValue = requireConcreteValue;
|
||||
_sb = new StringBuilder();
|
||||
|
||||
Parameters = new DynamicParameters();
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
Visit(filter);
|
||||
}
|
||||
}
|
||||
|
||||
public DynamicParameters Parameters { get; private set; }
|
||||
|
||||
private string AddParameter(object value, DbType? dbType = null)
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_paramCount++;
|
||||
var name = _paramNamePrefix + "_P" + _paramCount;
|
||||
Parameters.Add(name, value, dbType);
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
protected override Expression VisitBinary(BinaryExpression expression)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(expression.Left);
|
||||
|
||||
_sb.AppendFormat(" {0} ", Decode(expression));
|
||||
|
||||
Visit(expression.Right);
|
||||
|
||||
_sb.Append(')');
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMethodCall(MethodCallExpression expression)
|
||||
{
|
||||
var method = expression.Method.Name;
|
||||
|
||||
switch (expression.Method.Name)
|
||||
{
|
||||
case "Contains":
|
||||
ParseContainsExpression(expression);
|
||||
break;
|
||||
|
||||
case "StartsWith":
|
||||
ParseStartsWith(expression);
|
||||
break;
|
||||
|
||||
case "EndsWith":
|
||||
ParseEndsWith(expression);
|
||||
break;
|
||||
|
||||
default:
|
||||
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
|
||||
throw new NotImplementedException(msg);
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMemberAccess(MemberExpression expression)
|
||||
{
|
||||
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
|
||||
var gotValue = TryGetRightValue(expression, out var value);
|
||||
|
||||
// Only use the SQL condition if the expression didn't resolve to an actual value
|
||||
if (tableName != null && !gotValue)
|
||||
{
|
||||
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
// string is IEnumerable<Char> but we don't want to pick up that case
|
||||
var type = value.GetType();
|
||||
var typeInfo = type.GetTypeInfo();
|
||||
var isEnumerable =
|
||||
type != typeof(string) && (
|
||||
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
|
||||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
|
||||
|
||||
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
|
||||
_sb.Append(paramName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_sb.Append("NULL");
|
||||
}
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitConstant(ConstantExpression expression)
|
||||
{
|
||||
if (expression.Value != null)
|
||||
{
|
||||
var paramName = AddParameter(expression.Value);
|
||||
_sb.Append(paramName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_sb.Append("NULL");
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
private bool TryGetConstantValue(Expression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (expression is ConstantExpression constExp)
|
||||
{
|
||||
result = constExp.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetPropertyValue(MemberExpression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (expression.Expression is MemberExpression nested)
|
||||
{
|
||||
// Value is passed in as a property on a parent entity
|
||||
var container = (nested.Expression as ConstantExpression)?.Value;
|
||||
|
||||
if (container == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entity = GetFieldValue(container, nested.Member);
|
||||
result = GetFieldValue(entity, expression.Member);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetVariableValue(MemberExpression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
// Value is passed in as a variable
|
||||
if (expression.Expression is ConstantExpression nested)
|
||||
{
|
||||
result = GetFieldValue(nested.Value, expression.Member);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetRightValue(Expression expression, out object value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (TryGetConstantValue(expression, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var memberExp = expression as MemberExpression;
|
||||
|
||||
if (TryGetPropertyValue(memberExp, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetVariableValue(memberExp, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private object GetFieldValue(object entity, MemberInfo member)
|
||||
{
|
||||
if (member.MemberType == MemberTypes.Field)
|
||||
{
|
||||
return (member as FieldInfo).GetValue(entity);
|
||||
}
|
||||
|
||||
if (member.MemberType == MemberTypes.Property)
|
||||
{
|
||||
return (member as PropertyInfo).GetValue(entity);
|
||||
}
|
||||
|
||||
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
|
||||
}
|
||||
|
||||
private bool IsNullVariable(Expression expression)
|
||||
{
|
||||
if (expression.NodeType == ExpressionType.Constant &&
|
||||
TryGetConstantValue(expression, out var constResult) &&
|
||||
constResult == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expression.NodeType == ExpressionType.MemberAccess &&
|
||||
expression is MemberExpression member &&
|
||||
((TryGetPropertyValue(member, out var result) && result == null) ||
|
||||
(TryGetVariableValue(member, out result) && result == null)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string Decode(BinaryExpression expression)
|
||||
{
|
||||
if (IsNullVariable(expression.Right))
|
||||
{
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.Equal: return "IS";
|
||||
case ExpressionType.NotEqual: return "IS NOT";
|
||||
}
|
||||
}
|
||||
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.AndAlso: return "AND";
|
||||
case ExpressionType.And: return "AND";
|
||||
case ExpressionType.Equal: return "=";
|
||||
case ExpressionType.GreaterThan: return ">";
|
||||
case ExpressionType.GreaterThanOrEqual: return ">=";
|
||||
case ExpressionType.LessThan: return "<";
|
||||
case ExpressionType.LessThanOrEqual: return "<=";
|
||||
case ExpressionType.NotEqual: return "<>";
|
||||
case ExpressionType.OrElse: return "OR";
|
||||
case ExpressionType.Or: return "OR";
|
||||
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseContainsExpression(MethodCallExpression expression)
|
||||
{
|
||||
var list = expression.Object;
|
||||
|
||||
if (list != null && (list.Type == typeof(string)))
|
||||
{
|
||||
ParseStringContains(expression);
|
||||
return;
|
||||
}
|
||||
|
||||
ParseEnumerableContains(expression);
|
||||
}
|
||||
|
||||
private void ParseEnumerableContains(MethodCallExpression body)
|
||||
{
|
||||
// Fish out the list and the item to compare
|
||||
// It's in a different form for arrays and Lists
|
||||
var list = body.Object;
|
||||
Expression item;
|
||||
|
||||
if (list != null)
|
||||
{
|
||||
// Generic collection
|
||||
item = body.Arguments[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Static method
|
||||
// Must be Enumerable.Contains(source, item)
|
||||
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
|
||||
{
|
||||
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
|
||||
}
|
||||
|
||||
list = body.Arguments[0];
|
||||
item = body.Arguments[1];
|
||||
}
|
||||
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(item);
|
||||
|
||||
_sb.Append(" IN ");
|
||||
|
||||
// hardcode the integer list if it exists to bypass parameter limit
|
||||
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
|
||||
{
|
||||
var items = (IEnumerable<int>)value;
|
||||
_sb.Append('(');
|
||||
_sb.Append(string.Join(", ", items));
|
||||
_sb.Append(')');
|
||||
|
||||
_gotConcreteValue = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Visit(list);
|
||||
}
|
||||
|
||||
_sb.Append(')');
|
||||
}
|
||||
|
||||
private void ParseStringContains(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE '%' || ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(" || '%')");
|
||||
}
|
||||
|
||||
private void ParseStartsWith(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(" || '%')");
|
||||
}
|
||||
|
||||
private void ParseEndsWith(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE '%' || ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(')');
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sql = _sb.ToString();
|
||||
|
||||
if (_requireConcreteValue && !_gotConcreteValue)
|
||||
{
|
||||
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
|
||||
e.Data.Add("sql", sql);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
public DynamicParameters Parameters { get; protected set; }
|
||||
}
|
||||
}
|
||||
|
||||
374
src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs
Normal file
374
src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs
Normal file
@@ -0,0 +1,374 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class WhereBuilderPostgres : WhereBuilder
|
||||
{
|
||||
protected StringBuilder _sb;
|
||||
|
||||
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||
private readonly string _paramNamePrefix;
|
||||
private readonly bool _requireConcreteValue = false;
|
||||
private int _paramCount = 0;
|
||||
private bool _gotConcreteValue = false;
|
||||
|
||||
public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq)
|
||||
{
|
||||
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
|
||||
_requireConcreteValue = requireConcreteValue;
|
||||
_sb = new StringBuilder();
|
||||
|
||||
Parameters = new DynamicParameters();
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
Visit(filter);
|
||||
}
|
||||
}
|
||||
|
||||
private string AddParameter(object value, DbType? dbType = null)
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_paramCount++;
|
||||
var name = _paramNamePrefix + "_P" + _paramCount;
|
||||
Parameters.Add(name, value, dbType);
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
protected override Expression VisitBinary(BinaryExpression expression)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(expression.Left);
|
||||
|
||||
_sb.AppendFormat(" {0} ", Decode(expression));
|
||||
|
||||
Visit(expression.Right);
|
||||
|
||||
_sb.Append(')');
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMethodCall(MethodCallExpression expression)
|
||||
{
|
||||
var method = expression.Method.Name;
|
||||
|
||||
switch (expression.Method.Name)
|
||||
{
|
||||
case "Contains":
|
||||
ParseContainsExpression(expression);
|
||||
break;
|
||||
|
||||
case "StartsWith":
|
||||
ParseStartsWith(expression);
|
||||
break;
|
||||
|
||||
case "EndsWith":
|
||||
ParseEndsWith(expression);
|
||||
break;
|
||||
|
||||
default:
|
||||
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
|
||||
throw new NotImplementedException(msg);
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMemberAccess(MemberExpression expression)
|
||||
{
|
||||
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
|
||||
var gotValue = TryGetRightValue(expression, out var value);
|
||||
|
||||
// Only use the SQL condition if the expression didn't resolve to an actual value
|
||||
if (tableName != null && !gotValue)
|
||||
{
|
||||
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
// string is IEnumerable<Char> but we don't want to pick up that case
|
||||
var type = value.GetType();
|
||||
var typeInfo = type.GetTypeInfo();
|
||||
var isEnumerable =
|
||||
type != typeof(string) && (
|
||||
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
|
||||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
|
||||
|
||||
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
|
||||
_sb.Append(paramName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_sb.Append("NULL");
|
||||
}
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitConstant(ConstantExpression expression)
|
||||
{
|
||||
if (expression.Value != null)
|
||||
{
|
||||
var paramName = AddParameter(expression.Value);
|
||||
_sb.Append(paramName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_sb.Append("NULL");
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
private bool TryGetConstantValue(Expression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (expression is ConstantExpression constExp)
|
||||
{
|
||||
result = constExp.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetPropertyValue(MemberExpression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (expression.Expression is MemberExpression nested)
|
||||
{
|
||||
// Value is passed in as a property on a parent entity
|
||||
var container = (nested.Expression as ConstantExpression)?.Value;
|
||||
|
||||
if (container == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entity = GetFieldValue(container, nested.Member);
|
||||
result = GetFieldValue(entity, expression.Member);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetVariableValue(MemberExpression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
// Value is passed in as a variable
|
||||
if (expression.Expression is ConstantExpression nested)
|
||||
{
|
||||
result = GetFieldValue(nested.Value, expression.Member);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetRightValue(Expression expression, out object value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (TryGetConstantValue(expression, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var memberExp = expression as MemberExpression;
|
||||
|
||||
if (TryGetPropertyValue(memberExp, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetVariableValue(memberExp, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private object GetFieldValue(object entity, MemberInfo member)
|
||||
{
|
||||
if (member.MemberType == MemberTypes.Field)
|
||||
{
|
||||
return (member as FieldInfo).GetValue(entity);
|
||||
}
|
||||
|
||||
if (member.MemberType == MemberTypes.Property)
|
||||
{
|
||||
return (member as PropertyInfo).GetValue(entity);
|
||||
}
|
||||
|
||||
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
|
||||
}
|
||||
|
||||
private bool IsNullVariable(Expression expression)
|
||||
{
|
||||
if (expression.NodeType == ExpressionType.Constant &&
|
||||
TryGetConstantValue(expression, out var constResult) &&
|
||||
constResult == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expression.NodeType == ExpressionType.MemberAccess &&
|
||||
expression is MemberExpression member &&
|
||||
((TryGetPropertyValue(member, out var result) && result == null) ||
|
||||
(TryGetVariableValue(member, out result) && result == null)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string Decode(BinaryExpression expression)
|
||||
{
|
||||
if (IsNullVariable(expression.Right))
|
||||
{
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.Equal: return "IS";
|
||||
case ExpressionType.NotEqual: return "IS NOT";
|
||||
}
|
||||
}
|
||||
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.AndAlso: return "AND";
|
||||
case ExpressionType.And: return "AND";
|
||||
case ExpressionType.Equal: return "=";
|
||||
case ExpressionType.GreaterThan: return ">";
|
||||
case ExpressionType.GreaterThanOrEqual: return ">=";
|
||||
case ExpressionType.LessThan: return "<";
|
||||
case ExpressionType.LessThanOrEqual: return "<=";
|
||||
case ExpressionType.NotEqual: return "<>";
|
||||
case ExpressionType.OrElse: return "OR";
|
||||
case ExpressionType.Or: return "OR";
|
||||
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseContainsExpression(MethodCallExpression expression)
|
||||
{
|
||||
var list = expression.Object;
|
||||
|
||||
if (list != null && (list.Type == typeof(string)))
|
||||
{
|
||||
ParseStringContains(expression);
|
||||
return;
|
||||
}
|
||||
|
||||
ParseEnumerableContains(expression);
|
||||
}
|
||||
|
||||
private void ParseEnumerableContains(MethodCallExpression body)
|
||||
{
|
||||
// Fish out the list and the item to compare
|
||||
// It's in a different form for arrays and Lists
|
||||
var list = body.Object;
|
||||
Expression item;
|
||||
|
||||
if (list != null)
|
||||
{
|
||||
// Generic collection
|
||||
item = body.Arguments[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Static method
|
||||
// Must be Enumerable.Contains(source, item)
|
||||
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
|
||||
{
|
||||
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
|
||||
}
|
||||
|
||||
list = body.Arguments[0];
|
||||
item = body.Arguments[1];
|
||||
}
|
||||
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(item);
|
||||
|
||||
_sb.Append(" = ANY (");
|
||||
|
||||
Visit(list);
|
||||
|
||||
_sb.Append("))");
|
||||
}
|
||||
|
||||
private void ParseStringContains(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE '%' || ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(" || '%')");
|
||||
}
|
||||
|
||||
private void ParseStartsWith(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(" || '%')");
|
||||
}
|
||||
|
||||
private void ParseEndsWith(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE '%' || ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(')');
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sql = _sb.ToString();
|
||||
|
||||
if (_requireConcreteValue && !_gotConcreteValue)
|
||||
{
|
||||
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
|
||||
e.Data.Add("sql", sql);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
}
|
||||
}
|
||||
387
src/NzbDrone.Core/Datastore/WhereBuilderSqlite.cs
Normal file
387
src/NzbDrone.Core/Datastore/WhereBuilderSqlite.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class WhereBuilderSqlite : WhereBuilder
|
||||
{
|
||||
protected StringBuilder _sb;
|
||||
|
||||
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||
private readonly string _paramNamePrefix;
|
||||
private readonly bool _requireConcreteValue = false;
|
||||
private int _paramCount = 0;
|
||||
private bool _gotConcreteValue = false;
|
||||
|
||||
public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq)
|
||||
{
|
||||
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
|
||||
_requireConcreteValue = requireConcreteValue;
|
||||
_sb = new StringBuilder();
|
||||
|
||||
Parameters = new DynamicParameters();
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
Visit(filter);
|
||||
}
|
||||
}
|
||||
|
||||
private string AddParameter(object value, DbType? dbType = null)
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_paramCount++;
|
||||
var name = _paramNamePrefix + "_P" + _paramCount;
|
||||
Parameters.Add(name, value, dbType);
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
protected override Expression VisitBinary(BinaryExpression expression)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(expression.Left);
|
||||
|
||||
_sb.AppendFormat(" {0} ", Decode(expression));
|
||||
|
||||
Visit(expression.Right);
|
||||
|
||||
_sb.Append(')');
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMethodCall(MethodCallExpression expression)
|
||||
{
|
||||
var method = expression.Method.Name;
|
||||
|
||||
switch (expression.Method.Name)
|
||||
{
|
||||
case "Contains":
|
||||
ParseContainsExpression(expression);
|
||||
break;
|
||||
|
||||
case "StartsWith":
|
||||
ParseStartsWith(expression);
|
||||
break;
|
||||
|
||||
case "EndsWith":
|
||||
ParseEndsWith(expression);
|
||||
break;
|
||||
|
||||
default:
|
||||
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
|
||||
throw new NotImplementedException(msg);
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMemberAccess(MemberExpression expression)
|
||||
{
|
||||
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
|
||||
var gotValue = TryGetRightValue(expression, out var value);
|
||||
|
||||
// Only use the SQL condition if the expression didn't resolve to an actual value
|
||||
if (tableName != null && !gotValue)
|
||||
{
|
||||
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
// string is IEnumerable<Char> but we don't want to pick up that case
|
||||
var type = value.GetType();
|
||||
var typeInfo = type.GetTypeInfo();
|
||||
var isEnumerable =
|
||||
type != typeof(string) && (
|
||||
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
|
||||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
|
||||
|
||||
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
|
||||
_sb.Append(paramName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_sb.Append("NULL");
|
||||
}
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitConstant(ConstantExpression expression)
|
||||
{
|
||||
if (expression.Value != null)
|
||||
{
|
||||
var paramName = AddParameter(expression.Value);
|
||||
_sb.Append(paramName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_sb.Append("NULL");
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
private bool TryGetConstantValue(Expression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (expression is ConstantExpression constExp)
|
||||
{
|
||||
result = constExp.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetPropertyValue(MemberExpression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (expression.Expression is MemberExpression nested)
|
||||
{
|
||||
// Value is passed in as a property on a parent entity
|
||||
var container = (nested.Expression as ConstantExpression)?.Value;
|
||||
|
||||
if (container == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entity = GetFieldValue(container, nested.Member);
|
||||
result = GetFieldValue(entity, expression.Member);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetVariableValue(MemberExpression expression, out object result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
// Value is passed in as a variable
|
||||
if (expression.Expression is ConstantExpression nested)
|
||||
{
|
||||
result = GetFieldValue(nested.Value, expression.Member);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetRightValue(Expression expression, out object value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (TryGetConstantValue(expression, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var memberExp = expression as MemberExpression;
|
||||
|
||||
if (TryGetPropertyValue(memberExp, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetVariableValue(memberExp, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private object GetFieldValue(object entity, MemberInfo member)
|
||||
{
|
||||
if (member.MemberType == MemberTypes.Field)
|
||||
{
|
||||
return (member as FieldInfo).GetValue(entity);
|
||||
}
|
||||
|
||||
if (member.MemberType == MemberTypes.Property)
|
||||
{
|
||||
return (member as PropertyInfo).GetValue(entity);
|
||||
}
|
||||
|
||||
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
|
||||
}
|
||||
|
||||
private bool IsNullVariable(Expression expression)
|
||||
{
|
||||
if (expression.NodeType == ExpressionType.Constant &&
|
||||
TryGetConstantValue(expression, out var constResult) &&
|
||||
constResult == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expression.NodeType == ExpressionType.MemberAccess &&
|
||||
expression is MemberExpression member &&
|
||||
((TryGetPropertyValue(member, out var result) && result == null) ||
|
||||
(TryGetVariableValue(member, out result) && result == null)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string Decode(BinaryExpression expression)
|
||||
{
|
||||
if (IsNullVariable(expression.Right))
|
||||
{
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.Equal: return "IS";
|
||||
case ExpressionType.NotEqual: return "IS NOT";
|
||||
}
|
||||
}
|
||||
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.AndAlso: return "AND";
|
||||
case ExpressionType.And: return "AND";
|
||||
case ExpressionType.Equal: return "=";
|
||||
case ExpressionType.GreaterThan: return ">";
|
||||
case ExpressionType.GreaterThanOrEqual: return ">=";
|
||||
case ExpressionType.LessThan: return "<";
|
||||
case ExpressionType.LessThanOrEqual: return "<=";
|
||||
case ExpressionType.NotEqual: return "<>";
|
||||
case ExpressionType.OrElse: return "OR";
|
||||
case ExpressionType.Or: return "OR";
|
||||
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseContainsExpression(MethodCallExpression expression)
|
||||
{
|
||||
var list = expression.Object;
|
||||
|
||||
if (list != null && (list.Type == typeof(string)))
|
||||
{
|
||||
ParseStringContains(expression);
|
||||
return;
|
||||
}
|
||||
|
||||
ParseEnumerableContains(expression);
|
||||
}
|
||||
|
||||
private void ParseEnumerableContains(MethodCallExpression body)
|
||||
{
|
||||
// Fish out the list and the item to compare
|
||||
// It's in a different form for arrays and Lists
|
||||
var list = body.Object;
|
||||
Expression item;
|
||||
|
||||
if (list != null)
|
||||
{
|
||||
// Generic collection
|
||||
item = body.Arguments[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Static method
|
||||
// Must be Enumerable.Contains(source, item)
|
||||
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
|
||||
{
|
||||
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
|
||||
}
|
||||
|
||||
list = body.Arguments[0];
|
||||
item = body.Arguments[1];
|
||||
}
|
||||
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(item);
|
||||
|
||||
_sb.Append(" IN ");
|
||||
|
||||
// hardcode the integer list if it exists to bypass parameter limit
|
||||
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
|
||||
{
|
||||
var items = (IEnumerable<int>)value;
|
||||
_sb.Append('(');
|
||||
_sb.Append(string.Join(", ", items));
|
||||
_sb.Append(')');
|
||||
|
||||
_gotConcreteValue = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Visit(list);
|
||||
}
|
||||
|
||||
_sb.Append(')');
|
||||
}
|
||||
|
||||
private void ParseStringContains(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE '%' || ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(" || '%')");
|
||||
}
|
||||
|
||||
private void ParseStartsWith(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(" || '%')");
|
||||
}
|
||||
|
||||
private void ParseEndsWith(MethodCallExpression body)
|
||||
{
|
||||
_sb.Append('(');
|
||||
|
||||
Visit(body.Object);
|
||||
|
||||
_sb.Append(" LIKE '%' || ");
|
||||
|
||||
Visit(body.Arguments[0]);
|
||||
|
||||
_sb.Append(')');
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sql = _sb.ToString();
|
||||
|
||||
if (_requireConcreteValue && !_gotConcreteValue)
|
||||
{
|
||||
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
|
||||
e.Data.Add("sql", sql);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,11 +100,13 @@ namespace NzbDrone.Core.History
|
||||
|
||||
public int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes)
|
||||
{
|
||||
var builder = new SqlBuilder()
|
||||
var intEvents = eventTypes.Select(t => (int)t).ToList();
|
||||
|
||||
var builder = new SqlBuilder(_database.DatabaseType)
|
||||
.SelectCount()
|
||||
.Where<History>(x => x.IndexerId == indexerId)
|
||||
.Where<History>(x => x.Date >= date)
|
||||
.Where<History>(x => eventTypes.Contains(x.EventType));
|
||||
.Where<History>(x => intEvents.Contains((int)x.EventType));
|
||||
|
||||
var sql = builder.AddPageCountTemplate(typeof(History));
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
public class CleanupOrphanedApplicationStatus : IHousekeepingTask
|
||||
{
|
||||
private readonly IMainDatabase _database;
|
||||
|
||||
public CleanupOrphanedApplicationStatus(IMainDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
var mapper = _database.OpenConnection();
|
||||
|
||||
mapper.Execute(@"DELETE FROM ""ApplicationStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""ApplicationStatus"".""Id"" FROM ""ApplicationStatus""
|
||||
LEFT OUTER JOIN ""Applications""
|
||||
ON ""ApplicationStatus"".""ProviderId"" = ""Applications"".""Id""
|
||||
WHERE ""Applications"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,10 +91,12 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
r.IndexerFlags == null ? null : from f in r.IndexerFlags select GetNabElement("tag", f.Name, protocol),
|
||||
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c.Id, protocol),
|
||||
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c.Id, protocol),
|
||||
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
|
||||
GetNabElement("rageid", r.TvRageId, protocol),
|
||||
GetNabElement("tvdbid", r.TvdbId, protocol),
|
||||
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
|
||||
GetNabElement("tmdbid", r.TmdbId, protocol),
|
||||
GetNabElement("traktid", r.TraktId, protocol),
|
||||
GetNabElement("seeders", t.Seeders, protocol),
|
||||
GetNabElement("files", r.Files, protocol),
|
||||
GetNabElement("grabs", r.Grabs, protocol),
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
||||
|
||||
private const string DEFINITION_BRANCH = "master";
|
||||
private const int DEFINITION_VERSION = 3;
|
||||
private const int DEFINITION_VERSION = 5;
|
||||
|
||||
//Used when moving yml to C#
|
||||
private readonly List<string> _defintionBlocklist = new List<string>()
|
||||
@@ -204,7 +204,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
var dbDefs = _versionService.All();
|
||||
|
||||
//Check to ensure it's in versioned defs before we go to web
|
||||
if (dbDefs.Count > 0 && dbDefs.Any(x => x.File == fileKey))
|
||||
if (dbDefs.Count > 0 && !dbDefs.Any(x => x.File == fileKey))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileKey));
|
||||
}
|
||||
|
||||
@@ -48,14 +48,17 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "Anime Series");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.Audio, "Anime Musik/OST");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.PCGames, "Anime Spiele");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.XXX, "Hentai");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.PCGames, "Spiele Linux");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.XXX, "Anime Hentai");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.PCGames, "Software");
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.Other, "Sonstiges");
|
||||
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.Movies, "Filme");
|
||||
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.TV, "Serien");
|
||||
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.PCGames, "Spiele");
|
||||
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.Audio, "Musik");
|
||||
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.BooksComics, "Mangas");
|
||||
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.Movies, "Cartoon Filme");
|
||||
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.TV, "Cartoon Serie");
|
||||
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.XXX, "H-Manga / Doujinshi");
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{ "order_way", "desc" },
|
||||
{ "action", "basic" },
|
||||
{ "searchsubmit", "1" },
|
||||
{ "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term }
|
||||
{ "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term.Replace(".", " ") }
|
||||
};
|
||||
|
||||
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
|
||||
@@ -46,6 +46,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
public List<AvistazRelease> Data { get; set; }
|
||||
}
|
||||
|
||||
public class AvistazErrorResponse
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public class AvistazIdInfo
|
||||
{
|
||||
public string Tmdb { get; set; }
|
||||
|
||||
@@ -89,6 +89,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
await GetToken();
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.Warn(ex, "Unauthorized request to indexer");
|
||||
|
||||
var jsonResponse = new HttpResponse<AvistazErrorResponse>(ex.Response);
|
||||
return new ValidationFailure(string.Empty, jsonResponse.Resource?.Message ?? "Unauthorized request to indexer");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
@@ -182,18 +182,23 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerHttpResponse.StatusCode} code from API request");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
if (!indexerHttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerHttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerResponse.HttpResponse);
|
||||
if (indexerResponse.Content.ContainsIgnoreCase("Invalid API Key"))
|
||||
{
|
||||
throw new IndexerAuthException("API Key invalid or not authorized");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerHttpResponse);
|
||||
|
||||
foreach (var row in jsonResponse.Resource.Results)
|
||||
{
|
||||
@@ -213,7 +218,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Grabs = row.Grabs,
|
||||
Seeders = row.Seeders,
|
||||
ImdbId = ParseUtil.GetImdbID(row.ImdbId).GetValueOrDefault(),
|
||||
TmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.CoerceInt(row.TmdbId.Split("/")[1]),
|
||||
TmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : (int)ParseUtil.CoerceLong(row.TmdbId.Split("/")[1]),
|
||||
Peers = row.Leechers + row.Seeders,
|
||||
DownloadVolumeFactor = row.Freeleech || row.Limited ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
@@ -16,6 +17,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
public override bool SupportsSearch => true;
|
||||
public override int PageSize => 100;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "http://api.broadcasthe.net/" };
|
||||
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -25,8 +26,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var results = new List<ReleaseInfo>();
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
switch (indexerResponse.HttpResponse.StatusCode)
|
||||
switch (indexerHttpResponse.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
throw new IndexerAuthException("API Key invalid or not authorized");
|
||||
@@ -35,25 +37,30 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
case HttpStatusCode.ServiceUnavailable:
|
||||
throw new RequestLimitReachedException(indexerResponse, "Cannot do more than 150 API requests per hour.");
|
||||
default:
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode);
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerHttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/html"))
|
||||
if (indexerHttpResponse.Headers.ContentType != null && indexerHttpResponse.Headers.ContentType.Contains("text/html"))
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable.");
|
||||
}
|
||||
|
||||
if (indexerResponse.Content.ContainsIgnoreCase("Call Limit Exceeded"))
|
||||
{
|
||||
throw new RequestLimitReachedException(indexerResponse, "Cannot do more than 150 API requests per hour.");
|
||||
}
|
||||
|
||||
if (indexerResponse.Content == "Query execution was interrupted")
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API returned an internal server error");
|
||||
}
|
||||
|
||||
JsonRpcResponse<BroadcastheNetTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<BroadcastheNetTorrents>>(indexerResponse.HttpResponse).Resource;
|
||||
JsonRpcResponse<BroadcastheNetTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<BroadcastheNetTorrents>>(indexerHttpResponse).Resource;
|
||||
|
||||
if (jsonResponse.Error != null || jsonResponse.Result == null)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
protected static readonly Regex _LogicFunctionRegex = new Regex(
|
||||
$@"\b({string.Join("|", _SupportedLogicFunctions.Select(Regex.Escape))})(?:\s+(\(?\.[^\)\s]+\)?|""[^""]+"")){{2,}}");
|
||||
|
||||
// Matches CSS selectors for the JSON parser
|
||||
protected static readonly Regex _jsonSelectorRegex = new Regex(@"\:(?<filter>.+?)\((?<key>.+?)\)(?=:|\z)", RegexOptions.Compiled);
|
||||
|
||||
public CardigannSettings Settings { get; set; }
|
||||
|
||||
public CardigannBase(IConfigService configService,
|
||||
@@ -234,13 +237,20 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
if (selector.Selector != null)
|
||||
{
|
||||
var selector_Selector = ApplyGoTemplateText(selector.Selector.TrimStart('.'), variables);
|
||||
var selection = parentObj.SelectToken(selector_Selector);
|
||||
var selectorSelector = ApplyGoTemplateText(selector.Selector.TrimStart('.'), variables);
|
||||
selectorSelector = JsonParseFieldSelector(parentObj, selectorSelector);
|
||||
|
||||
JToken selection = null;
|
||||
if (selectorSelector != null)
|
||||
{
|
||||
selection = parentObj.SelectToken(selectorSelector);
|
||||
}
|
||||
|
||||
if (selection == null)
|
||||
{
|
||||
if (required)
|
||||
{
|
||||
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selector_Selector, parentObj.ToString()));
|
||||
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selectorSelector, parentObj.ToString()));
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -375,6 +385,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
return results;
|
||||
}
|
||||
|
||||
public ICollection<IndexerCategory> MapTrackerCatDescToNewznab(string trackerCategoryDesc)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(trackerCategoryDesc))
|
||||
{
|
||||
return new List<IndexerCategory>();
|
||||
}
|
||||
|
||||
var cats = _categoryMapping
|
||||
.Where(m =>
|
||||
!string.IsNullOrWhiteSpace(m.TrackerCategoryDesc) &&
|
||||
string.Equals(m.TrackerCategoryDesc, trackerCategoryDesc, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(c => NewznabStandardCategory.AllCats.FirstOrDefault(n => n.Id == c.NewzNabCategory) ?? new IndexerCategory { Id = c.NewzNabCategory })
|
||||
.ToList();
|
||||
return cats;
|
||||
}
|
||||
|
||||
protected delegate string TemplateTextModifier(string str);
|
||||
|
||||
protected string ApplyGoTemplateText(string template, Dictionary<string, object> variables = null, TemplateTextModifier modifier = null)
|
||||
@@ -816,5 +842,108 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
return new Uri(currentUrl ?? new Uri(SiteLink), path);
|
||||
}
|
||||
|
||||
protected string ResolveSiteLink()
|
||||
{
|
||||
var settingsBaseUrl = Settings?.BaseUrl;
|
||||
var defaultLink = _definition.Links.First();
|
||||
|
||||
if (settingsBaseUrl == null)
|
||||
{
|
||||
return defaultLink;
|
||||
}
|
||||
|
||||
if (_definition?.Legacylinks?.Contains(settingsBaseUrl) ?? false)
|
||||
{
|
||||
_logger.Trace("Changing legacy site link from {0} to {1}", settingsBaseUrl, defaultLink);
|
||||
return defaultLink;
|
||||
}
|
||||
|
||||
return settingsBaseUrl;
|
||||
}
|
||||
|
||||
protected JArray JsonParseRowsSelector(JToken parsedJson, string rowSelector)
|
||||
{
|
||||
var selector = rowSelector.Split(':')[0];
|
||||
var rowsObj = parsedJson.SelectToken(selector).Value<JArray>();
|
||||
return new JArray(rowsObj.Where(t =>
|
||||
JsonParseFieldSelector(t.Value<JObject>(), rowSelector.Remove(0, selector.Length)) != null));
|
||||
}
|
||||
|
||||
private string JsonParseFieldSelector(JToken parsedJson, string rowSelector)
|
||||
{
|
||||
var selector = rowSelector.Split(':')[0];
|
||||
JToken parsedObject;
|
||||
if (string.IsNullOrWhiteSpace(selector))
|
||||
{
|
||||
parsedObject = parsedJson;
|
||||
}
|
||||
else if (parsedJson.SelectToken(selector) != null)
|
||||
{
|
||||
parsedObject = parsedJson.SelectToken(selector);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (Match match in _jsonSelectorRegex.Matches(rowSelector))
|
||||
{
|
||||
var filter = match.Result("${filter}");
|
||||
var key = match.Result("${key}");
|
||||
Match innerMatch;
|
||||
switch (filter)
|
||||
{
|
||||
case "has":
|
||||
innerMatch = _jsonSelectorRegex.Match(key);
|
||||
if (innerMatch.Success)
|
||||
{
|
||||
if (JsonParseFieldSelector(parsedObject, key) == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parsedObject.SelectToken(key) == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case "not":
|
||||
innerMatch = _jsonSelectorRegex.Match(key);
|
||||
if (innerMatch.Success)
|
||||
{
|
||||
if (JsonParseFieldSelector(parsedObject, key) != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parsedObject.SelectToken(key) != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case "contains":
|
||||
if (!parsedObject.ToString().Contains(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported selector: {1}", _definition.Id, rowSelector));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public int After { get; set; }
|
||||
public SelectorBlock Dateheaders { get; set; }
|
||||
public SelectorBlock Count { get; set; }
|
||||
public bool Multiple { get; set; } = false;
|
||||
}
|
||||
|
||||
public class SearchPathBlock : RequestBlock
|
||||
@@ -200,8 +201,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public class ResponseBlock
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Attribute { get; set; }
|
||||
public bool Multiple { get; set; }
|
||||
public string NoResultsMessage { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
protected override string SiteLink => Settings?.BaseUrl ?? _definition.Links.First();
|
||||
protected override string SiteLink => ResolveSiteLink();
|
||||
|
||||
public CardigannParser(IConfigService configService,
|
||||
CardigannDefinition definition,
|
||||
@@ -83,16 +83,16 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
var rowsObj = parsedJson.SelectToken(search.Rows.Selector);
|
||||
if (rowsObj == null)
|
||||
var rowsArray = JsonParseRowsSelector(parsedJson, search.Rows.Selector);
|
||||
if (rowsArray == null)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Error Parsing Rows Selector");
|
||||
}
|
||||
|
||||
foreach (var row in rowsObj.Value<JArray>())
|
||||
foreach (var row in rowsArray)
|
||||
{
|
||||
var selObj = request.SearchPath.Response.Attribute != null ? row.SelectToken(request.SearchPath.Response.Attribute).Value<JToken>() : row;
|
||||
var mulRows = request.SearchPath.Response.Multiple == true ? selObj.Values<JObject>() : new List<JObject> { selObj.Value<JObject>() };
|
||||
var selObj = search.Rows.Attribute != null ? row.SelectToken(search.Rows.Attribute).Value<JToken>() : row;
|
||||
var mulRows = search.Rows.Multiple ? selObj.Values<JObject>() : new List<JObject> { selObj.Value<JObject>() };
|
||||
|
||||
foreach (var mulRow in mulRows)
|
||||
{
|
||||
@@ -464,6 +464,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
value = release.Categories.ToString();
|
||||
break;
|
||||
case "categorydesc":
|
||||
var catsDesc = MapTrackerCatDescToNewznab(value);
|
||||
if (catsDesc.Any())
|
||||
{
|
||||
if (release.Categories == null || fieldModifiers.Contains("noappend"))
|
||||
{
|
||||
release.Categories = catsDesc;
|
||||
}
|
||||
else
|
||||
{
|
||||
release.Categories = release.Categories.Union(catsDesc).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
value = release.Categories.ToString();
|
||||
break;
|
||||
case "size":
|
||||
@@ -545,6 +561,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
release.TvRageId = (int)ParseUtil.CoerceLong(rageID);
|
||||
value = release.TvRageId.ToString();
|
||||
break;
|
||||
case "traktid":
|
||||
var traktIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||
var traktIDMatch = traktIDRegEx.Match(value);
|
||||
var traktID = traktIDMatch.Groups[1].Value;
|
||||
release.TraktId = (int)ParseUtil.CoerceLong(traktID);
|
||||
value = release.TraktId.ToString();
|
||||
break;
|
||||
case "tvdbid":
|
||||
var tvdbIdRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||
var tvdbIdMatch = tvdbIdRegEx.Match(value);
|
||||
@@ -561,6 +584,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
value = release.PosterUrl;
|
||||
break;
|
||||
case "genre":
|
||||
release.Genres = release.Genres.Union(value.Split(',')).ToList();
|
||||
value = release.Genres.ToString();
|
||||
break;
|
||||
case "year":
|
||||
release.Year = ParseUtil.CoerceInt(value);
|
||||
value = release.Year.ToString();
|
||||
break;
|
||||
case "author":
|
||||
release.Author = value;
|
||||
break;
|
||||
|
||||
@@ -8,13 +8,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public Dictionary<string, object> Variables { get; private set; }
|
||||
public SearchPathBlock SearchPath { get; private set; }
|
||||
|
||||
public CardigannRequest(string url, HttpAccept httpAccept, Dictionary<string, object> variables, SearchPathBlock searchPath)
|
||||
: base(url, httpAccept)
|
||||
{
|
||||
Variables = variables;
|
||||
SearchPath = searchPath;
|
||||
}
|
||||
|
||||
public CardigannRequest(HttpRequest httpRequest, Dictionary<string, object> variables, SearchPathBlock searchPath)
|
||||
: base(httpRequest)
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
public IDictionary<string, string> Cookies { get; set; }
|
||||
protected HttpResponse landingResult;
|
||||
protected IHtmlDocument landingResultDocument;
|
||||
protected override string SiteLink => Settings?.BaseUrl ?? _definition.Links.First();
|
||||
protected override string SiteLink => ResolveSiteLink();
|
||||
|
||||
public CardigannRequestGenerator(IConfigService configService,
|
||||
CardigannDefinition definition,
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting search");
|
||||
_logger.Trace("Getting Movie search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
@@ -61,6 +61,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting Music search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
@@ -77,6 +79,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting TV search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
@@ -99,6 +103,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting Book search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
@@ -113,6 +119,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
_logger.Trace("Getting Basic search");
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var variables = GetQueryVariableDefaults(searchCriteria);
|
||||
@@ -647,10 +655,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
protected string GetRedirectDomainHint(string requestUrl, string redirectUrl)
|
||||
{
|
||||
if (requestUrl.StartsWith(SiteLink) && !redirectUrl.StartsWith(SiteLink))
|
||||
var siteLinkUri = new HttpUri(SiteLink);
|
||||
var requestUri = new HttpUri(requestUrl);
|
||||
var redirectUri = new HttpUri(redirectUrl);
|
||||
|
||||
if (requestUri.Host.StartsWith(siteLinkUri.Host) && !redirectUri.Host.StartsWith(siteLinkUri.Host))
|
||||
{
|
||||
var uri = new HttpUri(redirectUrl);
|
||||
return uri.Scheme + "://" + uri.Host + "/";
|
||||
return redirectUri.Scheme + "://" + redirectUri.Host + "/";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -19,6 +19,7 @@ using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[Obsolete("Moved to YML")]
|
||||
public class DanishBytes : TorrentIndexerBase<DanishBytesSettings>
|
||||
{
|
||||
public override string Name => "DanishBytes";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Gazelle
|
||||
{
|
||||
@@ -59,6 +60,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
||||
public int TotalSeeders { get; set; }
|
||||
public int TotalSnatched { get; set; }
|
||||
public long MaxSize { get; set; }
|
||||
[JsonConverter(typeof(GazelleTimestampConverter))]
|
||||
public long GroupTime { get; set; }
|
||||
public List<GazelleTorrent> Torrents { get; set; }
|
||||
public bool IsFreeLeech { get; set; }
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
parameters += string.Format("&searchstr={0}", searchString);
|
||||
parameters += string.Format("&searchstr={0}", searchString.Replace(".", " "));
|
||||
}
|
||||
|
||||
if (categories != null)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Gazelle
|
||||
{
|
||||
public class GazelleTimestampConverter : JsonConverter
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (objectType == typeof(long))
|
||||
{
|
||||
return Convert.ToInt64(reader.Value);
|
||||
}
|
||||
|
||||
if (objectType == typeof(string))
|
||||
{
|
||||
var date = DateTimeOffset.Parse(reader.Value.ToString());
|
||||
return date.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
throw new JsonSerializationException("Can't convert type " + existingValue.GetType().FullName + " to timestamp");
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(long) || objectType == typeof(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,29 +23,29 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
throw new IndexerException(indexerResponse,
|
||||
"Unexpected response status {0} code from API request",
|
||||
indexerResponse.HttpResponse.StatusCode);
|
||||
throw new RequestLimitReachedException(indexerResponse, "HDBits Query Limit Reached. Please try again later.");
|
||||
}
|
||||
|
||||
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Unexpected response status {0} code from API request", indexerHttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
var jsonResponse = JsonConvert.DeserializeObject<HDBitsResponse>(indexerResponse.Content);
|
||||
|
||||
if (jsonResponse.Status != StatusCode.Success)
|
||||
{
|
||||
throw new IndexerException(indexerResponse,
|
||||
"HDBits API request returned status code {0}: {1}",
|
||||
jsonResponse.Status,
|
||||
jsonResponse.Message ?? string.Empty);
|
||||
throw new IndexerException(indexerResponse, "HDBits API request returned status code {0}: {1}", jsonResponse.Status, jsonResponse.Message ?? string.Empty);
|
||||
}
|
||||
|
||||
var responseData = jsonResponse.Data as JArray;
|
||||
if (responseData == null)
|
||||
{
|
||||
throw new IndexerException(indexerResponse,
|
||||
"Indexer API call response missing result data");
|
||||
throw new IndexerException(indexerResponse, "Indexer API call response missing result data");
|
||||
}
|
||||
|
||||
var queryResults = responseData.ToObject<TorrentQueryResponse[]>();
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
else
|
||||
{
|
||||
queryCollection.Add("options", "0");
|
||||
queryCollection.Add("search", term);
|
||||
queryCollection.Add("search", term.Replace(".", " "));
|
||||
}
|
||||
|
||||
searchUrl += queryCollection.GetQueryString();
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "HD-Torrents";
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "https://hdts.ru/" };
|
||||
public override string[] IndexerUrls => new string[] { "https://hdts.ru/", "https://hd-torrents.org/" };
|
||||
public override string Description => "HD-Torrents is a private torrent website with HD torrents and strict rules on their content.";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
|
||||
@@ -170,16 +170,21 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
|
||||
{
|
||||
var searchUrl = Settings.BaseUrl + "browse.php";
|
||||
var searchUrl = Settings.BaseUrl + "browse.php?";
|
||||
|
||||
if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
searchUrl += string.Format("?do=search&keywords={0}&search_type=t_name&category=0&include_dead_torrents=no", WebUtility.UrlEncode(term));
|
||||
searchUrl += string.Format("do=search&keywords={0}&search_type=t_name&category=0&include_dead_torrents=no", WebUtility.UrlEncode(term));
|
||||
}
|
||||
|
||||
if (categories != null && categories.Length > 0)
|
||||
{
|
||||
searchUrl += "&selectedcats2=" + string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(categories));
|
||||
if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
searchUrl += "&";
|
||||
}
|
||||
|
||||
searchUrl += "selectedcats2=" + string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(categories));
|
||||
}
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
@@ -150,14 +150,14 @@ public class MoreThanTVRequestGenerator : IIndexerRequestGenerator
|
||||
qc.Add("filter_cat[2]", "1"); // SD Movies
|
||||
break;
|
||||
case TvSearchCriteria:
|
||||
qc.Add("filter_cat[3]", "1"); // HD EPISODE
|
||||
qc.Add("filter_cat[3]", "1"); // HD Episode
|
||||
qc.Add("filter_cat[4]", "1"); // SD Episode
|
||||
qc.Add("filter_cat[5]", "1"); // HD Season
|
||||
qc.Add("filter_cat[6]", "1"); // SD Season
|
||||
break;
|
||||
}
|
||||
|
||||
return $"{Settings.BaseUrl}torrents.php?{qc.GetQueryString()}";
|
||||
return $"{Settings.BaseUrl}torrents/browse?{qc.GetQueryString()}";
|
||||
}
|
||||
|
||||
private string GetSearchString(string input)
|
||||
@@ -188,28 +188,25 @@ public class MoreThanTVParser : IParseIndexerResponse
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
// Parse required data
|
||||
var torrentGroup = torrent.QuerySelectorAll("table a[href^=\"/torrents.php?action=download\"]");
|
||||
foreach (var downloadAnchor in torrentGroup)
|
||||
var downloadAnchor = torrent.QuerySelector("span a[href^=\"/torrents.php?action=download\"]");
|
||||
var title = downloadAnchor.ParentElement.ParentElement.ParentElement.QuerySelector("a[class=\"overlay_torrent\"]").TextContent.Trim();
|
||||
title = CleanUpTitle(title);
|
||||
|
||||
var category = torrent.QuerySelector(".cats_col div").GetAttribute("title");
|
||||
|
||||
// default to Other
|
||||
var indexerCategory = NewznabStandardCategory.Other;
|
||||
|
||||
if (movies.Any(category.Contains))
|
||||
{
|
||||
var title = downloadAnchor.ParentElement.ParentElement.ParentElement.TextContent.Trim();
|
||||
title = CleanUpTitle(title);
|
||||
|
||||
var category = torrent.QuerySelector(".cats_col div").GetAttribute("title");
|
||||
|
||||
// default to Other
|
||||
var indexerCategory = NewznabStandardCategory.Other;
|
||||
|
||||
if (movies.Any(category.Contains))
|
||||
{
|
||||
indexerCategory = NewznabStandardCategory.Movies;
|
||||
}
|
||||
else if (tv.Any(category.Contains))
|
||||
{
|
||||
indexerCategory = NewznabStandardCategory.TV;
|
||||
}
|
||||
|
||||
releases.Add(GetReleaseInfo(torrent, downloadAnchor, title, indexerCategory));
|
||||
indexerCategory = NewznabStandardCategory.Movies;
|
||||
}
|
||||
else if (tv.Any(category.Contains))
|
||||
{
|
||||
indexerCategory = NewznabStandardCategory.TV;
|
||||
}
|
||||
|
||||
releases.Add(GetReleaseInfo(torrent, downloadAnchor, title, indexerCategory));
|
||||
}
|
||||
|
||||
return releases;
|
||||
@@ -231,7 +228,7 @@ public class MoreThanTVParser : IParseIndexerResponse
|
||||
private ReleaseInfo GetReleaseInfo(IElement row, IElement downloadAnchor, string title, IndexerCategory category)
|
||||
{
|
||||
// count from bottom
|
||||
const int FILES_COL = 8;
|
||||
const int FILES_COL = 7;
|
||||
/*const int COMMENTS_COL = 7;*/
|
||||
const int DATE_COL = 6;
|
||||
const int FILESIZE_COL = 5;
|
||||
|
||||
@@ -25,26 +25,32 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
// Remove cookie cache
|
||||
if (indexerResponse.HttpResponse.HasHttpRedirect && indexerResponse.HttpResponse.RedirectUrl
|
||||
if (indexerHttpResponse.HasHttpRedirect && indexerHttpResponse.RedirectUrl
|
||||
.ContainsIgnoreCase("login.php"))
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the PTP login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
|
||||
throw new IndexerAuthException("We are being redirected to the PTP login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
|
||||
}
|
||||
|
||||
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
throw new RequestLimitReachedException(indexerResponse, "PTP Query Limit Reached. Please try again later.");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.Headers.ContentType != HttpAccept.Json.Value)
|
||||
if (indexerHttpResponse.Headers.ContentType != HttpAccept.Json.Value)
|
||||
{
|
||||
if (indexerResponse.HttpResponse.Request.Url.Path.ContainsIgnoreCase("login.php"))
|
||||
if (indexerHttpResponse.Request.Url.Path.ContainsIgnoreCase("login.php"))
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerException(indexerResponse, "We are currently on the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
|
||||
throw new IndexerAuthException("We are currently on the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
|
||||
}
|
||||
|
||||
// Remove cookie cache
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public override string[] IndexerUrls => new string[] { "https://pornolab.net/" };
|
||||
private string LoginUrl => Settings.BaseUrl + "forum/login.php";
|
||||
public override string Description => "PornoLab is a Semi-Private Russian site for Adult content";
|
||||
public override string Language => "ru-ru";
|
||||
public override string Language => "ru-RU";
|
||||
public override Encoding Encoding => Encoding.GetEncoding("windows-1251");
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPrivate;
|
||||
|
||||
@@ -73,8 +73,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.PC, "Applications");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksEBook, "E-Books");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.MoviesOther, "E-Learning Videos");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TVOther, "Comedy");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Other, "E-Learning Videos");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Other, "Comedy");
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.BooksComics, "Comics");
|
||||
|
||||
return caps;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Xthor
|
||||
{
|
||||
[Obsolete("Moved to YML for Cardigann v5")]
|
||||
public class Xthor : TorrentIndexerBase<XthorSettings>
|
||||
{
|
||||
public override string Name => "Xthor";
|
||||
|
||||
@@ -14,10 +14,10 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
private static readonly IndexerCommonSettingsValidator Validator = new IndexerCommonSettingsValidator();
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Number, Label = "Query Limit", HelpText = "The number of queries per day Prowlarr will allow to the site", Advanced = true)]
|
||||
[FieldDefinition(1, Type = FieldType.Number, Label = "Query Limit", HelpText = "The number of queries within a rolling 24 hour period Prowlarr will allow to the site", Advanced = true)]
|
||||
public int? QueryLimit { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Number, Label = "Grab Limit", HelpText = "The number of grabs per day Prowlarr will allow to the site", Advanced = true)]
|
||||
[FieldDefinition(2, Type = FieldType.Number, Label = "Grab Limit", HelpText = "The number of grabs within a rolling 24 hour period Prowlarr will allow to the site", Advanced = true)]
|
||||
public int? GrabLimit { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
"About": "نبدة عن",
|
||||
"AcceptConfirmationModal": "نموذج قبول التأكيد",
|
||||
"Actions": "أجراءات",
|
||||
"MinutesHundredTwenty": "120 دقيقة: {0}",
|
||||
"MonoVersion": "نسخة أحادية",
|
||||
"MoreInfo": "مزيد من المعلومات",
|
||||
"BranchUpdateMechanism": "يستخدم الفرع بواسطة آلية التحديث الخارجية",
|
||||
"CloneIndexer": "مفهرس استنساخ",
|
||||
"Added": "مضاف",
|
||||
"AddIndexer": "أضف مفهرس",
|
||||
"AddingTag": "مضيفا العلامة",
|
||||
@@ -18,7 +15,6 @@
|
||||
"AreYouSureYouWantToResetYourAPIKey": "هل أنت متأكد أنك تريد إعادة تعيين مفتاح API الخاص بك؟",
|
||||
"Authentication": "المصادقة",
|
||||
"BackupIntervalHelpText": "الفاصل الزمني بين النسخ الاحتياطية التلقائية",
|
||||
"Languages": "اللغات",
|
||||
"BackupNow": "اعمل نسخة احتياطية الان",
|
||||
"BackupRetentionHelpText": "سيتم تنظيف النسخ الاحتياطية التلقائية الأقدم من فترة الاحتفاظ تلقائيًا",
|
||||
"Backups": "النسخ الاحتياطية",
|
||||
@@ -32,18 +28,11 @@
|
||||
"DeleteApplicationMessageText": "هل تريد بالتأكيد حذف الإشعار \"{0}\"؟",
|
||||
"DeleteDownloadClient": "حذف Download Client",
|
||||
"DeleteDownloadClientMessageText": "هل أنت متأكد أنك تريد حذف برنامج التنزيل \"{0}\"؟",
|
||||
"DeleteIndexer": "حذف المفهرس",
|
||||
"DeleteIndexerMessageText": "هل أنت متأكد أنك تريد حذف المفهرس \"{0}\"؟",
|
||||
"EnableMediaInfoHelpText": "استخراج معلومات الفيديو مثل الدقة ووقت التشغيل ومعلومات الترميز من الملفات. يتطلب هذا من Radarr قراءة أجزاء من الملف والتي قد تسبب نشاطًا كبيرًا على القرص أو الشبكة أثناء عمليات الفحص.",
|
||||
"Filename": "اسم الملف",
|
||||
"HomePage": "الصفحة الرئيسية",
|
||||
"IndexerProxyStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات: {0}",
|
||||
"Language": "لغة",
|
||||
"MinutesNinety": "90 دقيقة: {0}",
|
||||
"Usenet": "يوزنت",
|
||||
"MinutesSixty": "60 دقيقة: {0}",
|
||||
"MonoVersionCheckUpgradeRecommendedMessage": "يتم دعم الإصدار Mono المثبت حاليًا {0} ولكن يوصى بالترقية إلى {1}.",
|
||||
"MovieDetailsNextMovie": "تفاصيل الفيلم: الفيلم القادم",
|
||||
"MovieIndexScrollTop": "فهرس الفيلم: قم بالتمرير لأعلى",
|
||||
"New": "جديد",
|
||||
"NoLinks": "لا روابط",
|
||||
@@ -66,7 +55,6 @@
|
||||
"ProxyPasswordHelpText": "ما عليك سوى إدخال اسم مستخدم وكلمة مرور إذا كان أحدهما مطلوبًا. اتركها فارغة وإلا.",
|
||||
"ProxyUsernameHelpText": "ما عليك سوى إدخال اسم مستخدم وكلمة مرور إذا كان أحدهما مطلوبًا. اتركها فارغة وإلا.",
|
||||
"PtpOldSettingsCheckMessage": "مفهرسات PassThePopcorn التالية بها إعدادات مهملة ويجب تحديثها: {0}",
|
||||
"QualitySettings": "إعدادات الجودة",
|
||||
"Queue": "طابور",
|
||||
"ReadTheWikiForMoreInformation": "اقرأ Wiki لمزيد من المعلومات",
|
||||
"Reddit": "رديت",
|
||||
@@ -83,19 +71,12 @@
|
||||
"UnableToAddANewDownloadClientPleaseTryAgain": "غير قادر على إضافة عميل تنزيل جديد ، يرجى المحاولة مرة أخرى.",
|
||||
"UnableToAddANewIndexerPleaseTryAgain": "غير قادر على إضافة مفهرس جديد ، يرجى المحاولة مرة أخرى.",
|
||||
"UnableToLoadBackups": "تعذر تحميل النسخ الاحتياطية",
|
||||
"UnableToLoadIndexers": "تعذر تحميل المفهرسات",
|
||||
"UnableToLoadQualityDefinitions": "تعذر تحميل تعريفات الجودة",
|
||||
"UnsavedChanges": "التغييرات غير المحفوظة",
|
||||
"UpdateCheckUINotWritableMessage": "لا يمكن تثبيت التحديث لأن مجلد واجهة المستخدم '{0}' غير قابل للكتابة بواسطة المستخدم '{1}'",
|
||||
"UpdateScriptPathHelpText": "المسار إلى برنامج نصي مخصص يأخذ حزمة تحديث مستخرجة ويتعامل مع ما تبقى من عملية التحديث",
|
||||
"Username": "اسم المستخدم",
|
||||
"Warn": "حذر",
|
||||
"Yesterday": "في الامس",
|
||||
"EnableColorImpairedMode": "تفعيل وضع ضعف الألوان",
|
||||
"EnableColorImpairedModeHelpText": "تم تغيير النمط للسماح للمستخدمين الذين يعانون من ضعف الألوان بتمييز المعلومات المشفرة بالألوان بشكل أفضل",
|
||||
"EnableCompletedDownloadHandlingHelpText": "استيراد التنزيلات المكتملة تلقائيًا من عميل التنزيل",
|
||||
"EnabledHelpText": "قم بتمكين هذه القائمة للاستخدام في Radarr",
|
||||
"EnableHelpText": "تفعيل إنشاء ملف البيانات الوصفية لنوع البيانات الوصفية هذا",
|
||||
"EnableInteractiveSearch": "تمكين البحث التفاعلي",
|
||||
"Source": "مصدر",
|
||||
"SSLCertPassword": "كلمة مرور شهادة SSL",
|
||||
@@ -107,19 +88,15 @@
|
||||
"CouldNotConnectSignalR": "تعذر الاتصال بـ SignalR ، لن يتم تحديث واجهة المستخدم",
|
||||
"Dates": "تواريخ",
|
||||
"DBMigration": "ترحيل DB",
|
||||
"DelayProfile": "ملف التأخير",
|
||||
"Delete": "حذف",
|
||||
"Details": "تفاصيل",
|
||||
"Donations": "التبرعات",
|
||||
"DownloadClient": "تحميل العميل",
|
||||
"EnableAutoHelpText": "في حالة التمكين ، ستتم إضافة الأفلام تلقائيًا إلى Radarr من هذه القائمة",
|
||||
"IndexerStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل",
|
||||
"IndexerStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات: {0}",
|
||||
"Info": "معلومات",
|
||||
"Interval": "فترة",
|
||||
"Manual": "كتيب",
|
||||
"MaximumLimits": "الحدود القصوى",
|
||||
"NoLimitForAnyRuntime": "لا يوجد حد لأي وقت تشغيل",
|
||||
"General": "جنرال لواء",
|
||||
"GeneralSettings": "الاعدادات العامة",
|
||||
"Grabbed": "اقتطف",
|
||||
@@ -168,7 +145,6 @@
|
||||
"IndexerProxyStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل",
|
||||
"NoChange": "لا تغيير",
|
||||
"NoLogFiles": "لا توجد ملفات سجل",
|
||||
"NoMinimumForAnyRuntime": "لا يوجد حد أدنى لأي وقت تشغيل",
|
||||
"NoTagsHaveBeenAddedYet": "لم يتم إضافة أي علامات حتى الان",
|
||||
"NotificationTriggers": "مشغلات الإخطار",
|
||||
"NoUpdatesAreAvailable": "لا توجد تحديثات متوفرة",
|
||||
@@ -195,10 +171,8 @@
|
||||
"Clear": "واضح",
|
||||
"ClientPriority": "أولوية العميل",
|
||||
"Tomorrow": "غدا",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "تعذر الاتصال بـ {0}.",
|
||||
"Torrent": "السيول",
|
||||
"Torrents": "السيول",
|
||||
"ExistingMovies": "فيلم (أفلام) موجود",
|
||||
"ExistingTag": "علامة موجودة",
|
||||
"Failed": "فشل",
|
||||
"FeatureRequests": "طلبات مخصصة",
|
||||
@@ -210,23 +184,18 @@
|
||||
"History": "التاريخ",
|
||||
"Hostname": "اسم المضيف",
|
||||
"IgnoredAddresses": "العناوين التي تم تجاهلها",
|
||||
"Importing": "استيراد",
|
||||
"UILanguage": "لغة واجهة المستخدم",
|
||||
"UILanguageHelpText": "اللغة التي سيستخدمها Radarr لواجهة المستخدم",
|
||||
"UILanguageHelpTextWarning": "يلزم إعادة تحميل المتصفح",
|
||||
"KeyboardShortcuts": "اختصارات لوحة المفاتيح",
|
||||
"LogFiles": "ملفات الدخول",
|
||||
"LogLevelTraceHelpTextWarning": "يجب تمكين تسجيل التتبع مؤقتًا فقط",
|
||||
"MonoTlsCheckMessage": "لا يزال الحل البديل لـ Radarr Mono 4.x tls ممكّنًا ، فجرّب إزالة MONO_TLS_PROVIDER = خيار البيئة القديمة",
|
||||
"Movies": "أفلام",
|
||||
"Name": "اسم",
|
||||
"NetCore": ".شبكة",
|
||||
"Password": "كلمه السر",
|
||||
"PreferredSize": "الحجم المفضل",
|
||||
"Proxy": "الوكيل",
|
||||
"ProxyCheckResolveIpMessage": "فشل حل عنوان IP لمضيف الخادم الوكيل المكون {0}",
|
||||
"ProxyType": "نوع الوكيل",
|
||||
"QualityDefinitions": "تعريفات الجودة",
|
||||
"ReleaseStatus": "حالة الإصدار",
|
||||
"Search": "بحث",
|
||||
"SettingsShortDateFormat": "تنسيق التاريخ القصير",
|
||||
@@ -254,16 +223,11 @@
|
||||
"DownloadClients": "تحميل العملاء",
|
||||
"DownloadClientStatusCheckAllClientMessage": "جميع عملاء التنزيل غير متاحين بسبب الفشل",
|
||||
"DownloadClientStatusCheckSingleClientMessage": "برامج التنزيل غير متاحة بسبب الإخفاقات: {0}",
|
||||
"DownloadClientUnavailable": "عميل التنزيل غير متوفر",
|
||||
"Downloading": "جارى التحميل",
|
||||
"Edit": "تعديل",
|
||||
"EditIndexer": "تحرير المفهرس",
|
||||
"Enable": "ممكن",
|
||||
"EnableAutomaticAdd": "تمكين إضافة تلقائية",
|
||||
"EnableAutomaticSearch": "تمكين البحث التلقائي",
|
||||
"EnableAutomaticSearchHelpTextWarning": "سيتم استخدامها عند استخدام البحث التفاعلي",
|
||||
"Enabled": "ممكن",
|
||||
"EnableInteractiveSearchHelpTextWarning": "البحث غير معتمد مع هذا المفهرس",
|
||||
"IndexerPriority": "أولوية المفهرس",
|
||||
"Port": "ميناء",
|
||||
"SettingsTimeFormat": "تنسيق الوقت",
|
||||
@@ -292,12 +256,10 @@
|
||||
"SettingsLongDateFormat": "تنسيق التاريخ الطويل",
|
||||
"ConnectionLost": "انقطع الاتصال",
|
||||
"Connections": "روابط",
|
||||
"MovieDetailsPreviousMovie": "تفاصيل الفيلم: الفيلم السابق",
|
||||
"MovieIndexScrollBottom": "فهرس الفيلم: التمرير لأسفل",
|
||||
"NoBackupsAreAvailable": "لا توجد نسخ احتياطية متاحة",
|
||||
"NoChanges": "لا تغيرات",
|
||||
"NoLeaveIt": "لا ، اتركها",
|
||||
"Pending": "قيد الانتظار",
|
||||
"PendingChangesDiscardChanges": "تجاهل التغييرات واترك",
|
||||
"RSS": "RSS",
|
||||
"System": "النظام",
|
||||
@@ -328,7 +290,6 @@
|
||||
"Mechanism": "آلية",
|
||||
"Message": "رسالة",
|
||||
"MIA": "MIA",
|
||||
"MinimumLimits": "الحد الأدنى",
|
||||
"RefreshMovie": "تحديث الفيلم",
|
||||
"EnableAutomaticSearchHelpText": "سيتم استخدامه عند إجراء عمليات البحث التلقائي عبر واجهة المستخدم أو بواسطة Radarr",
|
||||
"Status": "الحالة",
|
||||
@@ -336,7 +297,6 @@
|
||||
"ApplyTagsHelpTexts4": "استبدال: استبدل العلامات بالعلامات التي تم إدخالها (لا تدخل أي علامات لمسح جميع العلامات)",
|
||||
"AuthenticationMethodHelpText": "طلب اسم المستخدم وكلمة المرور للوصول إلى Radarr",
|
||||
"Automatic": "تلقائي",
|
||||
"DownloadClientCheckNoneAvailableMessage": "لا يوجد عميل تنزيل متاح",
|
||||
"Mode": "الوضع",
|
||||
"Options": "خيارات",
|
||||
"Ok": "موافق",
|
||||
@@ -347,7 +307,6 @@
|
||||
"RestartRequiredHelpTextWarning": "يتطلب إعادة التشغيل ليصبح ساري المفعول",
|
||||
"Restore": "استعادة",
|
||||
"RestoreBackup": "استرجاع النسخة الاحتياطية",
|
||||
"Restrictions": "قيود",
|
||||
"Result": "نتيجة",
|
||||
"Retention": "احتفاظ",
|
||||
"SaveChanges": "حفظ التغييرات",
|
||||
@@ -358,5 +317,12 @@
|
||||
"Test": "اختبار",
|
||||
"TestAll": "اختبار الكل",
|
||||
"TestAllClients": "اختبر كل العملاء",
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "تعذر إضافة إشعار جديد ، يرجى المحاولة مرة أخرى."
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "تعذر إضافة إشعار جديد ، يرجى المحاولة مرة أخرى.",
|
||||
"MaintenanceRelease": "إصدار الصيانة: إصلاحات الأخطاء والتحسينات الأخرى. راجع Github Commit History لمزيد من التفاصيل",
|
||||
"Filters": "منقي",
|
||||
"HistoryCleanupDaysHelpText": "اضبط على 0 لتعطيل التنظيف التلقائي",
|
||||
"HistoryCleanupDaysHelpTextWarning": "سيتم تنظيف الملفات الموجودة في سلة المحذوفات الأقدم من عدد الأيام المحدد تلقائيًا",
|
||||
"OnGrab": "عند الاستيلاء",
|
||||
"OnHealthIssue": "في قضية الصحة",
|
||||
"TestAllIndexers": "اختبار كافة المفهرسات"
|
||||
}
|
||||
|
||||
@@ -42,11 +42,9 @@
|
||||
"Delete": "Изтрий",
|
||||
"DeleteDownloadClient": "Изтриване на клиент за изтегляне",
|
||||
"DeleteDownloadClientMessageText": "Наистина ли искате да изтриете клиента за изтегляне '{0}'?",
|
||||
"DeleteIndexer": "Изтрийте Indexer",
|
||||
"DeleteNotification": "Изтриване на известието",
|
||||
"DeleteNotificationMessageText": "Наистина ли искате да изтриете известието '{0}'?",
|
||||
"Donations": "Дарения",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "Не може да се комуникира с {0}.",
|
||||
"About": "относно",
|
||||
"AcceptConfirmationModal": "Приемете модал за потвърждение",
|
||||
"AddDownloadClient": "Добавяне на клиент за изтегляне",
|
||||
@@ -66,20 +64,15 @@
|
||||
"Connections": "Връзки",
|
||||
"CouldNotConnectSignalR": "Не можах да се свържа със SignalR, потребителският интерфейс няма да се актуализира",
|
||||
"DeleteBackupMessageText": "Наистина ли искате да изтриете резервното копие '{0}'?",
|
||||
"DeleteIndexerMessageText": "Наистина ли искате да изтриете индексатора '{0}'?",
|
||||
"DeleteTag": "Изтриване на маркера",
|
||||
"DeleteTagMessageText": "Наистина ли искате да изтриете маркера '{0}'?",
|
||||
"DownloadClientCheckNoneAvailableMessage": "Няма наличен клиент за изтегляне",
|
||||
"IndexerProxyStatusCheckAllClientMessage": "Всички списъци са недостъпни поради неуспехи",
|
||||
"LastWriteTime": "Време за последно писане",
|
||||
"Columns": "Колони",
|
||||
"EnableRss": "Активиране на RSS",
|
||||
"Downloading": "Изтегляне",
|
||||
"IndexerProxyStatusCheckSingleClientMessage": "Списъци, недостъпни поради неуспехи: {0}",
|
||||
"Languages": "Езици",
|
||||
"LogLevel": "Log Level",
|
||||
"MovieIndexScrollTop": "Индекс на филма: Превъртете отгоре",
|
||||
"Movies": "Филми",
|
||||
"Queue": "Опашка",
|
||||
"ReadTheWikiForMoreInformation": "Прочетете Wiki за повече информация",
|
||||
"Reddit": "Reddit",
|
||||
@@ -89,7 +82,6 @@
|
||||
"RemovedFromTaskQueue": "Премахнато от опашката на задачите",
|
||||
"RemoveFilter": "Отстранете филтъра",
|
||||
"RestoreBackup": "Възстанови архива",
|
||||
"Restrictions": "Ограничения",
|
||||
"Result": "Резултат",
|
||||
"Save": "Запазете",
|
||||
"SaveChanges": "Запазите промените",
|
||||
@@ -114,7 +106,6 @@
|
||||
"UnableToAddANewIndexerPleaseTryAgain": "Не може да се добави нов индексатор, моля, опитайте отново.",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "Не може да се добави нов индексатор, моля, опитайте отново.",
|
||||
"UnableToAddANewNotificationPleaseTryAgain": "Не може да се добави ново известие, моля, опитайте отново.",
|
||||
"UnableToLoadQualityDefinitions": "Определенията за качество не могат да се заредят",
|
||||
"UpdateAutomaticallyHelpText": "Автоматично изтегляне и инсталиране на актуализации. Все още ще можете да инсталирате от System: Updates",
|
||||
"UpdateCheckStartupTranslocationMessage": "Не може да се инсталира актуализация, защото стартовата папка „{0}“ е в папка за преместване на приложения.",
|
||||
"ReleaseStatus": "Състояние на освобождаване",
|
||||
@@ -129,7 +120,6 @@
|
||||
"StartupDirectory": "Стартова директория",
|
||||
"Disabled": "хора с увреждания",
|
||||
"EditIndexer": "Редактиране на Indexer",
|
||||
"EnableAutomaticSearchHelpTextWarning": "Ще се използва, когато се използва интерактивно търсене",
|
||||
"Enabled": "Активирано",
|
||||
"General": "Общ",
|
||||
"Grabbed": "Грабната",
|
||||
@@ -148,21 +138,17 @@
|
||||
"Language": "Език",
|
||||
"LogLevelTraceHelpTextWarning": "Регистрацията на проследяване трябва да бъде разрешена само временно",
|
||||
"Manual": "Ръчно",
|
||||
"MaximumLimits": "Максимални граници",
|
||||
"Mechanism": "Механизъм",
|
||||
"Message": "Съобщение",
|
||||
"MIA": "МВР",
|
||||
"MinutesHundredTwenty": "120 минути: {0}",
|
||||
"NoChange": "Няма промяна",
|
||||
"NoChanges": "Без промени",
|
||||
"NoLeaveIt": "Не, оставете го",
|
||||
"NoLogFiles": "Няма регистрационни файлове",
|
||||
"NoMinimumForAnyRuntime": "Няма минимум за всяко време на изпълнение",
|
||||
"RestartRequiredHelpTextWarning": "Изисква рестартиране, за да влезе в сила",
|
||||
"SettingsShortDateFormat": "Формат на кратка дата",
|
||||
"SSLCertPath": "SSL Cert Path",
|
||||
"URLBase": "URL база",
|
||||
"CloneIndexer": "Clone Indexer",
|
||||
"CloneProfile": "Профил на клониране",
|
||||
"EnableInteractiveSearch": "Активирайте интерактивно търсене",
|
||||
"TagIsNotUsedAndCanBeDeleted": "Етикетът не се използва и може да бъде изтрит",
|
||||
@@ -175,7 +161,6 @@
|
||||
"UnableToLoadBackups": "Архивите не могат да се заредят",
|
||||
"AllIndexersHiddenDueToFilter": "Всички филми са скрити поради приложен филтър.",
|
||||
"Level": "Ниво",
|
||||
"MinimumLimits": "Минимални лимити",
|
||||
"ApplicationStatusCheckAllClientMessage": "Всички списъци са недостъпни поради неуспехи",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Списъци, недостъпни поради неуспехи: {0}",
|
||||
"Close": "Близо",
|
||||
@@ -204,7 +189,6 @@
|
||||
"PackageVersion": "Версия на пакета",
|
||||
"PageSize": "Размер на страницата",
|
||||
"PendingChangesStayReview": "Останете и прегледайте промените",
|
||||
"PreferredSize": "Предпочитан размер",
|
||||
"Presets": "Предварителни настройки",
|
||||
"Priority": "Приоритет",
|
||||
"PrioritySettings": "Приоритет",
|
||||
@@ -212,13 +196,8 @@
|
||||
"SettingsLongDateFormat": "Формат с дълга дата",
|
||||
"SettingsShowRelativeDates": "Показване на относителни дати",
|
||||
"UnableToLoadDownloadClients": "Клиентите за изтегляне не могат да се заредят",
|
||||
"DelayProfile": "Профил за забавяне",
|
||||
"Logging": "Регистрация",
|
||||
"Exception": "Изключение",
|
||||
"MinutesNinety": "90 минути: {0}",
|
||||
"MinutesSixty": "60 минути: {0}",
|
||||
"MovieDetailsNextMovie": "Подробности за филма: Следващ филм",
|
||||
"MovieDetailsPreviousMovie": "Подробности за филма: Предишен филм",
|
||||
"MovieIndexScrollBottom": "Индекс на филма: Превъртане отдолу",
|
||||
"Name": "Име",
|
||||
"New": "Ново",
|
||||
@@ -233,12 +212,8 @@
|
||||
"ProxyCheckResolveIpMessage": "Неуспешно разрешаване на IP адреса за конфигурирания прокси хост {0}",
|
||||
"ProxyPasswordHelpText": "Трябва само да въведете потребителско име и парола, ако е необходимо. В противен случай ги оставете празни.",
|
||||
"ProxyType": "Тип прокси",
|
||||
"ExistingMovies": "Съществуващи филми",
|
||||
"ExistingTag": "Съществуващ маркер",
|
||||
"ProxyUsernameHelpText": "Трябва само да въведете потребителско име и парола, ако е необходимо. В противен случай ги оставете празни.",
|
||||
"QualityDefinitions": "Определения за качество",
|
||||
"QualitySettings": "Настройки за качество",
|
||||
"Importing": "Импортиране",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "Клон {0} не е валиден клон за издаване на Radarr, няма да получавате актуализации",
|
||||
"IncludeHealthWarningsHelpText": "Включете здравни предупреждения",
|
||||
"SaveSettings": "Запазване на настройките",
|
||||
@@ -275,8 +250,6 @@
|
||||
"UpdateCheckStartupNotWritableMessage": "Не може да се инсталира актуализация, тъй като стартовата папка „{0}“ не може да се записва от потребителя „{1}“.",
|
||||
"UpdateCheckUINotWritableMessage": "Не може да се инсталира актуализация, защото папката „{0}“ на потребителския интерфейс не може да се записва от потребителя „{1}“.",
|
||||
"Enable": "Активиране",
|
||||
"EnableColorImpairedMode": "Активирайте режима с увредени цветове",
|
||||
"EnableColorImpairedModeHelpText": "Променен стил, за да позволи на потребителите с увредени цветове да разграничат по-добре информацията, кодирана в цвят",
|
||||
"EventType": "Тип на събитието",
|
||||
"Failed": "Се провали",
|
||||
"FeatureRequests": "Заявки за функции",
|
||||
@@ -298,10 +271,6 @@
|
||||
"Indexers": "Индексатори",
|
||||
"IndexerStatusCheckAllClientMessage": "Всички индексатори са недостъпни поради грешки",
|
||||
"Mode": "Режим",
|
||||
"MonoTlsCheckMessage": "Работното решение на Radarr Mono 4.x tls все още е активирано, помислете за премахване на MONO_TLS_PROVIDER = опция за наследствена среда",
|
||||
"MonoVersion": "Моно версия",
|
||||
"MonoVersionCheckUpgradeRecommendedMessage": "Понастоящем инсталираната моно версия {0} се поддържа, но се препоръчва надстройка до {1}.",
|
||||
"NoLimitForAnyRuntime": "Няма ограничение за всяко време на изпълнение",
|
||||
"TagsSettingsSummary": "Вижте всички тагове и как се използват. Неизползваните маркери могат да бъдат премахнати",
|
||||
"TestAllClients": "Тествайте всички клиенти",
|
||||
"UnableToAddANewDownloadClientPleaseTryAgain": "Не може да се добави нов клиент за изтегляне, моля, опитайте отново.",
|
||||
@@ -315,17 +284,9 @@
|
||||
"DownloadClientSettings": "Изтеглете настройките на клиента",
|
||||
"DownloadClientStatusCheckAllClientMessage": "Всички клиенти за изтегляне са недостъпни поради неуспехи",
|
||||
"DownloadClientStatusCheckSingleClientMessage": "Клиентите за изтегляне са недостъпни поради грешки: {0}",
|
||||
"DownloadClientUnavailable": "Клиентът за изтегляне не е наличен",
|
||||
"Edit": "редактиране",
|
||||
"EnableAutoHelpText": "Ако е активирано, Филмите ще бъдат автоматично добавени към Radarr от този списък",
|
||||
"EnableAutomaticAdd": "Активирайте автоматичното добавяне",
|
||||
"EnableAutomaticSearch": "Активирайте автоматичното търсене",
|
||||
"EnableAutomaticSearchHelpText": "Ще се използва, когато се извършват автоматични търсения чрез потребителския интерфейс или от Radarr",
|
||||
"EnableCompletedDownloadHandlingHelpText": "Автоматично импортирайте завършени изтегляния от клиент за изтегляне",
|
||||
"EnabledHelpText": "Активирайте този списък за използване в Radarr",
|
||||
"EnableHelpText": "Активирайте създаването на файл с метаданни за този тип метаданни",
|
||||
"EnableInteractiveSearchHelpTextWarning": "Търсенето не се поддържа с този индексатор",
|
||||
"EnableMediaInfoHelpText": "Извличайте видео информация като резолюция, време на работа и информация за кодеци от файлове. Това изисква от Radarr да чете части от файла, които могат да причинят висока активност на диска или мрежата по време на сканиране.",
|
||||
"EnableSSL": "Активирайте SSL",
|
||||
"EnableSslHelpText": " Изисква рестартиране, изпълнено като администратор, за да влезе в сила",
|
||||
"Error": "Грешка",
|
||||
@@ -350,12 +311,17 @@
|
||||
"PageSizeHelpText": "Брой елементи за показване на всяка страница",
|
||||
"Password": "Парола",
|
||||
"Peers": "Връстници",
|
||||
"Pending": "В очакване",
|
||||
"PendingChangesDiscardChanges": "Изхвърлете промените и оставете",
|
||||
"PriorityHelpText": "Приоритизирайте множество клиенти за изтегляне. Round-Robin се използва за клиенти със същия приоритет.",
|
||||
"Protocol": "Протокол",
|
||||
"UnableToLoadGeneralSettings": "Не може да се заредят общи настройки",
|
||||
"UnableToLoadHistory": "Историята не може да се зареди",
|
||||
"UnableToLoadIndexers": "Индексаторите не могат да се заредят",
|
||||
"UnableToLoadNotifications": "Известията не могат да се заредят"
|
||||
"UnableToLoadNotifications": "Известията не могат да се заредят",
|
||||
"MaintenanceRelease": "Издание за поддръжка: поправки на грешки и други подобрения. Вижте История на комисиите на Github за повече подробности",
|
||||
"Filters": "Филтър",
|
||||
"HistoryCleanupDaysHelpText": "Задайте на 0, за да деактивирате автоматичното почистване",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Файловете в кошчето, по-стари от избрания брой дни, ще се почистват автоматично",
|
||||
"OnGrab": "На Граб",
|
||||
"OnHealthIssue": "По здравен въпрос",
|
||||
"TestAllIndexers": "Тествайте всички индексатори"
|
||||
}
|
||||
|
||||
1
src/NzbDrone.Core/Localization/Core/bn.json
Normal file
1
src/NzbDrone.Core/Localization/Core/bn.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -4,7 +4,6 @@
|
||||
"Actions": "Accions",
|
||||
"AcceptConfirmationModal": "Accepta el mètode de confirmació",
|
||||
"About": "Quant a",
|
||||
"Movies": "Pel·lícula",
|
||||
"New": "Nou",
|
||||
"Reload": "Recarregar",
|
||||
"Queue": "Cua",
|
||||
|
||||
@@ -6,19 +6,16 @@
|
||||
"About": "O",
|
||||
"Component": "Součástka",
|
||||
"Info": "Info",
|
||||
"Languages": "Jazyky",
|
||||
"LogFiles": "Záznam souborů",
|
||||
"PrioritySettings": "Přednost",
|
||||
"Logs": "Protokoly",
|
||||
"ProxyBypassFilterHelpText": "Použijte ',' jako oddělovač a '*.' jako zástupný znak pro subdomény",
|
||||
"QualityDefinitions": "Definice kvality",
|
||||
"SaveSettings": "Uložit nastavení",
|
||||
"Scheduled": "Naplánováno",
|
||||
"ScriptPath": "Cesta skriptu",
|
||||
"SetTags": "Nastavit značky",
|
||||
"Settings": "Nastavení",
|
||||
"StartTypingOrSelectAPathBelow": "Začněte psát nebo vyberte cestu níže",
|
||||
"UnableToLoadIndexers": "Nelze načíst indexery",
|
||||
"Usenet": "Usenet",
|
||||
"AddDownloadClient": "Přidat staženého klienta",
|
||||
"Backups": "Zálohy",
|
||||
@@ -26,10 +23,8 @@
|
||||
"MovieIndexScrollBottom": "Rejstřík filmů: Posun dolů",
|
||||
"ProxyType": "Typ serveru proxy",
|
||||
"Reddit": "Reddit",
|
||||
"DownloadClientUnavailable": "Stahovací klient není k dispozici",
|
||||
"ErrorLoadingContents": "Chyba při načítání obsahu",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Všechny indexery nejsou k dispozici z důvodu selhání po dobu delší než 6 hodin",
|
||||
"MonoVersion": "Mono verze",
|
||||
"RemovedFromTaskQueue": "Odebráno z fronty úkolů",
|
||||
"ResetAPIKey": "Resetovat klíč API",
|
||||
"SSLCertPassword": "Heslo SSL Cert",
|
||||
@@ -49,7 +44,6 @@
|
||||
"DownloadClientSettings": "Stáhněte si nastavení klienta",
|
||||
"DownloadClientStatusCheckAllClientMessage": "Všichni klienti pro stahování nejsou kvůli chybám k dispozici",
|
||||
"DownloadClientStatusCheckSingleClientMessage": "Stahování klientů není k dispozici z důvodu selhání: {0}",
|
||||
"Downloading": "Stahování",
|
||||
"Folder": "Složka",
|
||||
"Grabs": "Urvat",
|
||||
"HealthNoIssues": "Žádné problémy s vaší konfigurací",
|
||||
@@ -68,12 +62,10 @@
|
||||
"Level": "Úroveň",
|
||||
"LogLevel": "Úroveň protokolu",
|
||||
"Manual": "Manuál",
|
||||
"MaximumLimits": "Maximální limity",
|
||||
"Message": "Zpráva",
|
||||
"MIA": "MIA",
|
||||
"Mode": "Režim",
|
||||
"NoTagsHaveBeenAddedYet": "Zatím nebyly přidány žádné značky",
|
||||
"NoMinimumForAnyRuntime": "Žádné minimum za běhu",
|
||||
"Ok": "OK",
|
||||
"SendAnonymousUsageData": "Odesílejte anonymní údaje o používání",
|
||||
"UnselectAll": "Odznačit vše",
|
||||
@@ -91,10 +83,6 @@
|
||||
"Branch": "Větev",
|
||||
"BranchUpdate": "Pobočka, která se má použít k aktualizaci Radarr",
|
||||
"EditIndexer": "Upravit indexátor",
|
||||
"EnableColorImpairedModeHelpText": "Upravený styl umožňující uživatelům s barevným postižením lépe rozlišovat barevně kódované informace",
|
||||
"EnableCompletedDownloadHandlingHelpText": "Automaticky importovat dokončená stahování z klienta pro stahování",
|
||||
"EnabledHelpText": "Povolte tento seznam pro použití v Radarru",
|
||||
"EnableHelpText": "Povolit vytváření souborů metadat pro tento typ metadat",
|
||||
"ForMoreInformationOnTheIndividualDownloadClients": "Další informace o jednotlivých klientech pro stahování získáte kliknutím na informační tlačítka.",
|
||||
"General": "Všeobecné",
|
||||
"ApplyTagsHelpTexts3": "Odebrat: Odebere zadané značky",
|
||||
@@ -103,7 +91,6 @@
|
||||
"ConnectionLost": "Spojení ztraceno",
|
||||
"ConnectSettings": "Připojit nastavení",
|
||||
"Custom": "Zvyk",
|
||||
"EnableAutomaticAdd": "Povolit automatické přidání",
|
||||
"Error": "Chyba",
|
||||
"Failed": "Selhalo",
|
||||
"FeatureRequests": "Žádosti o funkce",
|
||||
@@ -127,8 +114,6 @@
|
||||
"ApplyTags": "Použít značky",
|
||||
"MoreInfo": "Více informací",
|
||||
"System": "Systém",
|
||||
"EnableAutomaticSearchHelpTextWarning": "Bude použito při použití interaktivního vyhledávání",
|
||||
"EnableColorImpairedMode": "Aktivujte režim se sníženou barevností",
|
||||
"Enabled": "Povoleno",
|
||||
"IgnoredAddresses": "Ignorované adresy",
|
||||
"AcceptConfirmationModal": "Přijměte potvrzení Modal",
|
||||
@@ -138,16 +123,7 @@
|
||||
"LaunchBrowserHelpText": " Otevřete webový prohlížeč a při spuštění aplikace přejděte na domovskou stránku Radarr.",
|
||||
"Logging": "Protokolování",
|
||||
"Mechanism": "Mechanismus",
|
||||
"MinutesNinety": "90 minut: {0}",
|
||||
"MinutesSixty": "60 minut: {0}",
|
||||
"MonoTlsCheckMessage": "Řešení Radarr Mono 4.x tls je stále povoleno, zvažte odebrání MONO_TLS_PROVIDER = možnost staršího prostředí",
|
||||
"MonoVersionCheckUpgradeRecommendedMessage": "Aktuálně nainstalovaná mono verze {0} je podporována, ale doporučuje se upgradovat na {1}.",
|
||||
"MovieDetailsNextMovie": "Detaily filmu: Další film",
|
||||
"MovieDetailsPreviousMovie": "Detaily filmu: Předchozí film",
|
||||
"Movies": "Filmy",
|
||||
"NoLimitForAnyRuntime": "Žádné omezení za běhu",
|
||||
"NoLinks": "Žádné odkazy",
|
||||
"PreferredSize": "Preferovaná velikost",
|
||||
"Presets": "Předvolby",
|
||||
"Priority": "Přednost",
|
||||
"PriorityHelpText": "Upřednostněte více klientů pro stahování. Round-Robin se používá pro klienty se stejnou prioritou.",
|
||||
@@ -160,7 +136,6 @@
|
||||
"ProxyPasswordHelpText": "Musíte pouze zadat uživatelské jméno a heslo, pokud je požadováno. Jinak je nechte prázdné.",
|
||||
"ProxyUsernameHelpText": "Musíte pouze zadat uživatelské jméno a heslo, pokud je požadováno. Jinak je nechte prázdné.",
|
||||
"PtpOldSettingsCheckMessage": "Následující indexovače PassThePopcorn mají zastaralá nastavení a měla by být aktualizována: {0}",
|
||||
"QualitySettings": "Nastavení kvality",
|
||||
"Queue": "Fronta",
|
||||
"ReadTheWikiForMoreInformation": "Další informace najdete na Wiki",
|
||||
"Refresh": "Obnovit",
|
||||
@@ -193,7 +168,6 @@
|
||||
"UnableToAddANewIndexerPleaseTryAgain": "Nelze přidat nový indexer, zkuste to znovu.",
|
||||
"UnableToAddANewIndexerProxyPleaseTryAgain": "Nelze přidat nový indexer, zkuste to znovu.",
|
||||
"UnableToLoadNotifications": "Nelze načíst oznámení",
|
||||
"UnableToLoadQualityDefinitions": "Nelze načíst definice kvality",
|
||||
"UpdateCheckStartupTranslocationMessage": "Aktualizaci nelze nainstalovat, protože spouštěcí složka „{0}“ je ve složce Translocation aplikace.",
|
||||
"UpdateCheckUINotWritableMessage": "Aktualizaci nelze nainstalovat, protože uživatelská složka „{0}“ není zapisovatelná uživatelem „{1}“.",
|
||||
"UpdateMechanismHelpText": "Použijte vestavěný aktualizátor Radarr nebo skript",
|
||||
@@ -226,7 +200,6 @@
|
||||
"Restart": "Restartujte",
|
||||
"RestartNow": "Restartovat nyní",
|
||||
"RestoreBackup": "Obnovit zálohu",
|
||||
"Restrictions": "Omezení",
|
||||
"RSSIsNotSupportedWithThisIndexer": "RSS není u tohoto indexeru podporováno",
|
||||
"Save": "Uložit",
|
||||
"SSLCertPasswordHelpText": "Heslo pro soubor pfx",
|
||||
@@ -235,8 +208,6 @@
|
||||
"UnableToLoadBackups": "Nelze načíst zálohy",
|
||||
"UnableToLoadDownloadClients": "Nelze načíst klienty pro stahování",
|
||||
"UnableToLoadGeneralSettings": "Nelze načíst obecná nastavení",
|
||||
"DeleteIndexer": "Odstranit indexer",
|
||||
"DeleteIndexerMessageText": "Opravdu chcete odstranit indexer „{0}“?",
|
||||
"DeleteNotification": "Smazat oznámení",
|
||||
"EnableAutomaticSearch": "Povolit automatické vyhledávání",
|
||||
"EnableInteractiveSearchHelpText": "Bude použito při použití interaktivního vyhledávání",
|
||||
@@ -245,8 +216,6 @@
|
||||
"Interval": "Interval",
|
||||
"KeyboardShortcuts": "Klávesové zkratky",
|
||||
"Language": "Jazyk",
|
||||
"MinimumLimits": "Minimální limity",
|
||||
"MinutesHundredTwenty": "120 minut: {0}",
|
||||
"NoLogFiles": "Žádné soubory protokolu",
|
||||
"NoBackupsAreAvailable": "Nejsou k dispozici žádné zálohy",
|
||||
"NoChanges": "Žádné změny",
|
||||
@@ -278,7 +247,6 @@
|
||||
"ChangeHasNotBeenSavedYet": "Změna ještě nebyla uložena",
|
||||
"Clear": "Průhledná",
|
||||
"ClientPriority": "Priorita klienta",
|
||||
"CloneIndexer": "Klonovat indexátor",
|
||||
"CloneProfile": "Klonovat profil",
|
||||
"Close": "Zavřít",
|
||||
"ConnectionLostAutomaticMessage": "Radarr se pokusí připojit automaticky, nebo můžete kliknout na znovu načíst níže.",
|
||||
@@ -287,7 +255,6 @@
|
||||
"Date": "datum",
|
||||
"Dates": "Termíny",
|
||||
"DBMigration": "Migrace databáze",
|
||||
"DelayProfile": "Zpožděný profil",
|
||||
"Delete": "Vymazat",
|
||||
"DeleteApplicationMessageText": "Opravdu chcete smazat oznámení „{0}“?",
|
||||
"DeleteBackup": "Odstranit zálohu",
|
||||
@@ -296,25 +263,18 @@
|
||||
"DeleteTagMessageText": "Opravdu chcete smazat značku „{0}“?",
|
||||
"Discord": "Svár",
|
||||
"DownloadClient": "Stáhnout klienta",
|
||||
"DownloadClientCheckNoneAvailableMessage": "Není k dispozici žádný klient pro stahování",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "S uživatelem {0} nelze komunikovat.",
|
||||
"DownloadClients": "Stáhnout klienty",
|
||||
"Edit": "Upravit",
|
||||
"Enable": "Umožnit",
|
||||
"EnableAutoHelpText": "Pokud je tato možnost povolena, filmy budou automaticky přidány do Radarru z tohoto seznamu",
|
||||
"EnableAutomaticSearchHelpText": "Použije se, když se automatické vyhledávání provádí pomocí uživatelského rozhraní nebo Radarr",
|
||||
"EnableInteractiveSearch": "Povolit interaktivní vyhledávání",
|
||||
"EnableInteractiveSearchHelpTextWarning": "Vyhledávání není u tohoto indexeru podporováno",
|
||||
"EnableMediaInfoHelpText": "Extrahujte ze souborů informace o videu, jako je rozlišení, runtime a informace o kodeku. To vyžaduje, aby Radarr četl části souboru, což může během skenování způsobit vysokou aktivitu disku nebo sítě.",
|
||||
"EnableSSL": "Povolit SSL",
|
||||
"EnableSslHelpText": " Vyžaduje restartování spuštěné jako správce, aby se projevilo",
|
||||
"Events": "Události",
|
||||
"EventType": "Typ události",
|
||||
"Exception": "Výjimka",
|
||||
"ExistingMovies": "Stávající filmy",
|
||||
"ExistingTag": "Stávající značka",
|
||||
"IllRestartLater": "Restartuji později",
|
||||
"Importing": "Import",
|
||||
"IndexerLongTermStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání po dobu delší než 6 hodin: {0}",
|
||||
"IndexerStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání: {0}",
|
||||
"SettingsTimeFormat": "Časový formát",
|
||||
@@ -335,7 +295,6 @@
|
||||
"PageSizeHelpText": "Počet položek, které se mají zobrazit na každé stránce",
|
||||
"Password": "Heslo",
|
||||
"Peers": "Vrstevníci",
|
||||
"Pending": "čekající",
|
||||
"PendingChangesDiscardChanges": "Zahodit změny a odejít",
|
||||
"PortNumber": "Číslo portu",
|
||||
"Result": "Výsledek",
|
||||
@@ -357,5 +316,14 @@
|
||||
"UISettings": "Nastavení uživatelského rozhraní",
|
||||
"UnableToLoadUISettings": "Nelze načíst nastavení uživatelského rozhraní",
|
||||
"UnsavedChanges": "Neuložené změny",
|
||||
"UpdateAutomaticallyHelpText": "Automaticky stahovat a instalovat aktualizace. Stále budete moci instalovat ze systému: Aktualizace"
|
||||
"UpdateAutomaticallyHelpText": "Automaticky stahovat a instalovat aktualizace. Stále budete moci instalovat ze systému: Aktualizace",
|
||||
"NetCore": ".NET Core",
|
||||
"Filters": "Filtr",
|
||||
"ConnectionLostMessage": "Radarr ztratil spojení s back-endem a pro obnovení funkčnosti bude nutné jej znovu načíst.",
|
||||
"HistoryCleanupDaysHelpText": "Nastavením na 0 zakážete automatické čištění",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Soubory v koši starší než vybraný počet dní budou automaticky vyčištěny",
|
||||
"MaintenanceRelease": "Údržbové vydání: opravy chyb a další vylepšení. Další podrobnosti najdete v GitHub Commit History",
|
||||
"OnGrab": "Chyť",
|
||||
"OnHealthIssue": "K otázce zdraví",
|
||||
"TestAllIndexers": "Vyzkoušejte všechny indexery"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"LastWriteTime": "Sidste Skrive Tid",
|
||||
"Languages": "Sprog",
|
||||
"Language": "Sprog",
|
||||
"KeyboardShortcuts": "Keyboard Genveje",
|
||||
"Info": "Information",
|
||||
@@ -26,8 +25,6 @@
|
||||
"DownloadClientStatusCheckAllClientMessage": "Alle download klienter er utilgængelige på grund af fejl",
|
||||
"DownloadClientsSettingsSummary": "Download klienter, download håndtering og remote path mappings",
|
||||
"DownloadClients": "Download Klienter",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "Ude af stand til at kommunikere med {0}.",
|
||||
"DownloadClientCheckNoneAvailableMessage": "Ingen download klient tilgængelig",
|
||||
"DownloadClient": "Download Klient",
|
||||
"Details": "Detaljer",
|
||||
"Delete": "Slet",
|
||||
@@ -59,10 +56,7 @@
|
||||
"Add": "Tilføj",
|
||||
"AddDownloadClient": "Tilføj downloadklient",
|
||||
"DBMigration": "DB Migration",
|
||||
"DelayProfile": "Udskyd Profiler",
|
||||
"MIA": "MIA",
|
||||
"MovieDetailsNextMovie": "Filmoplysninger: Næste film",
|
||||
"MovieDetailsPreviousMovie": "Filmdetaljer: Forrige film",
|
||||
"ResetAPIKey": "Nulstil API-nøgle",
|
||||
"SettingsTimeFormat": "Tidsformat",
|
||||
"SystemTimeCheckMessage": "Systemtiden er slukket mere end 1 dag. Planlagte opgaver kører muligvis ikke korrekt, før tiden er rettet",
|
||||
@@ -109,13 +103,10 @@
|
||||
"BypassProxyForLocalAddresses": "Bypass-proxy til lokale adresser",
|
||||
"CancelPendingTask": "Er du sikker på, at du vil annullere denne afventende opgave?",
|
||||
"CertificateValidation": "Validering af certifikat",
|
||||
"CloneIndexer": "Klonindekser",
|
||||
"CloseCurrentModal": "Luk Nuværende Modal",
|
||||
"CouldNotConnectSignalR": "Kunne ikke oprette forbindelse til SignalR, UI opdateres ikke",
|
||||
"DeleteApplicationMessageText": "Er du sikker på, at du vil slette underretningen '{0}'?",
|
||||
"DeleteDownloadClientMessageText": "Er du sikker på, at du vil slette downloadklienten '{0}'?",
|
||||
"DeleteIndexer": "Slet Indexer",
|
||||
"DeleteIndexerMessageText": "Er du sikker på, at du vil slette indeksøren '{0}'?",
|
||||
"DeleteIndexerProxyMessageText": "Er du sikker på, at du vil slette tagget '{0}'?",
|
||||
"DeleteNotification": "Slet underretning",
|
||||
"DeleteNotificationMessageText": "Er du sikker på, at du vil slette underretningen '{0}'?",
|
||||
@@ -125,27 +116,16 @@
|
||||
"Docker": "Docker",
|
||||
"Donations": "Donationer",
|
||||
"DownloadClientSettings": "Download klientindstillinger",
|
||||
"DownloadClientUnavailable": "Downloadklienten er ikke tilgængelig",
|
||||
"Downloading": "Downloader",
|
||||
"EditIndexer": "Rediger indekser",
|
||||
"Enable": "Aktiver",
|
||||
"EnableAutoHelpText": "Hvis det er aktiveret, føjes film automatisk til Radarr fra denne liste",
|
||||
"EnableAutomaticAdd": "Aktivér automatisk tilføjelse",
|
||||
"EnableAutomaticSearch": "Aktivér automatisk søgning",
|
||||
"EnableAutomaticSearchHelpText": "Bruges, når der foretages automatiske søgninger via brugergrænsefladen eller af Radarr",
|
||||
"EnableAutomaticSearchHelpTextWarning": "Bruges, når der bruges interaktiv søgning",
|
||||
"EnableColorImpairedMode": "Aktivér farve-nedsat tilstand",
|
||||
"EnableColorImpairedModeHelpText": "Ændret stil for at give farvehæmmede brugere bedre at skelne mellem farvekodede oplysninger",
|
||||
"EnableCompletedDownloadHandlingHelpText": "Importer automatisk afsluttede downloads fra downloadklienten",
|
||||
"Enabled": "Aktiveret",
|
||||
"EnableHelpText": "Aktivér oprettelse af metadatafiler for denne metadatatype",
|
||||
"EnableInteractiveSearch": "Aktivér interaktiv søgning",
|
||||
"EnableMediaInfoHelpText": "Uddrag videoinformation såsom opløsning, runtime og codec-oplysninger fra filer. Dette kræver, at Radarr læser dele af filen, som kan forårsage høj disk- eller netværksaktivitet under scanninger.",
|
||||
"EnableRss": "Aktivér RSS",
|
||||
"EnableSslHelpText": " Kræver genstart, der kører som administrator for at træde i kraft",
|
||||
"ErrorLoadingContents": "Fejl ved indlæsning af indhold",
|
||||
"Exception": "Undtagelse",
|
||||
"ExistingMovies": "Eksisterende film",
|
||||
"ExistingTag": "Eksisterende mærke",
|
||||
"FeatureRequests": "Funktionsanmodninger",
|
||||
"Filter": "Filter",
|
||||
@@ -158,7 +138,6 @@
|
||||
"Hostname": "Værtsnavn",
|
||||
"IgnoredAddresses": "Ignorerede adresser",
|
||||
"IllRestartLater": "Jeg genstarter senere",
|
||||
"Importing": "Importerer",
|
||||
"IncludeHealthWarningsHelpText": "Inkluder sundhedsadvarsler",
|
||||
"Indexer": "Indekser",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Alle indeksatorer er ikke tilgængelige på grund af fejl i mere end 6 timer",
|
||||
@@ -173,17 +152,9 @@
|
||||
"LogLevelTraceHelpTextWarning": "Sporlogning bør kun aktiveres midlertidigt",
|
||||
"Logs": "Logfiler",
|
||||
"Manual": "brugervejledning",
|
||||
"MaximumLimits": "Maksimale grænser",
|
||||
"Mechanism": "Mekanisme",
|
||||
"Message": "Besked",
|
||||
"MinimumLimits": "Minimumsgrænser",
|
||||
"MinutesHundredTwenty": "120 minutter: {0}",
|
||||
"MinutesNinety": "90 minutter: {0}",
|
||||
"MinutesSixty": "60 minutter: {0}",
|
||||
"Mode": "Mode",
|
||||
"MonoTlsCheckMessage": "Radarr Mono 4.x tls-løsning er stadig aktiveret, overvej at fjerne MONO_TLS_PROVIDER = ældre miljømulighed",
|
||||
"MonoVersion": "Mono-version",
|
||||
"MonoVersionCheckUpgradeRecommendedMessage": "Aktuelt installeret Mono-version {0} understøttes, men det anbefales at opgradere til {1}.",
|
||||
"MovieIndexScrollBottom": "Filmindeks: Rul ned",
|
||||
"MovieIndexScrollTop": "Filmindeks: Scroll Top",
|
||||
"Name": "Navn",
|
||||
@@ -191,23 +162,19 @@
|
||||
"NoBackupsAreAvailable": "Ingen sikkerhedskopier er tilgængelige",
|
||||
"NoChanges": "Ingen ændringer",
|
||||
"NoLeaveIt": "Nej, lad det være",
|
||||
"NoLimitForAnyRuntime": "Ingen grænse for nogen runtime",
|
||||
"NoLinks": "Ingen links",
|
||||
"NoLogFiles": "Ingen logfiler",
|
||||
"NoMinimumForAnyRuntime": "Intet minimum for enhver driftstid",
|
||||
"NoTagsHaveBeenAddedYet": "Der er ikke tilføjet nogen tags endnu",
|
||||
"NotificationTriggers": "Meddelelsesudløsere",
|
||||
"OnHealthIssueHelpText": "Om sundhedsspørgsmål",
|
||||
"OpenThisModal": "Åbn denne modal",
|
||||
"PackageVersion": "Pakkeversion",
|
||||
"PageSize": "Sidestørrelse",
|
||||
"Pending": "Verserende",
|
||||
"PendingChangesDiscardChanges": "Kassér ændringer og gå ud",
|
||||
"PendingChangesMessage": "Du har ikke gemte ændringer. Er du sikker på, at du vil forlade denne side?",
|
||||
"PendingChangesStayReview": "Bliv og gennemgå ændringer",
|
||||
"Port": "Havn",
|
||||
"PortNumber": "Portnummer",
|
||||
"PreferredSize": "Foretrukken størrelse",
|
||||
"Priority": "Prioritet",
|
||||
"Protocol": "Protokol",
|
||||
"ProxyBypassFilterHelpText": "Brug ',' som en separator og '*.' som et jokertegn for underdomæner",
|
||||
@@ -217,8 +184,6 @@
|
||||
"ProxyPasswordHelpText": "Du skal kun indtaste et brugernavn og en adgangskode, hvis der kræves et. Lad dem være tomme ellers.",
|
||||
"ProxyUsernameHelpText": "Du skal kun indtaste et brugernavn og en adgangskode, hvis der kræves et. Lad dem være tomme ellers.",
|
||||
"PtpOldSettingsCheckMessage": "Følgende PassThePopcorn-indeksatorer har forældede indstillinger og skal opdateres: {0}",
|
||||
"QualityDefinitions": "Kvalitetsdefinitioner",
|
||||
"QualitySettings": "Kvalitetsindstillinger",
|
||||
"ReadTheWikiForMoreInformation": "Læs Wiki for mere information",
|
||||
"Reddit": "Reddit",
|
||||
"Refresh": "Opdater",
|
||||
@@ -275,7 +240,6 @@
|
||||
"UnableToAddANewIndexerPleaseTryAgain": "Kunne ikke tilføje en ny indekser. Prøv igen.",
|
||||
"UnableToLoadBackups": "Kunne ikke indlæse sikkerhedskopier",
|
||||
"UnableToLoadGeneralSettings": "Kan ikke indlæse generelle indstillinger",
|
||||
"UnableToLoadIndexers": "Kan ikke indlæse indeksatorer",
|
||||
"UnableToLoadNotifications": "Kunne ikke indlæse meddelelser",
|
||||
"UnableToLoadTags": "Kan ikke indlæse tags",
|
||||
"UnableToLoadUISettings": "UI-indstillingerne kunne ikke indlæses",
|
||||
@@ -297,7 +261,6 @@
|
||||
"ReleaseStatus": "Frigør status",
|
||||
"Restore": "Gendan",
|
||||
"RestoreBackup": "Gendan sikkerhedskopi",
|
||||
"Restrictions": "Begrænsninger",
|
||||
"RSS": "RSS",
|
||||
"RSSIsNotSupportedWithThisIndexer": "RSS understøttes ikke med denne indekseringsenhed",
|
||||
"Save": "Gemme",
|
||||
@@ -310,12 +273,10 @@
|
||||
"StartupDirectory": "Startmappe",
|
||||
"Status": "Status",
|
||||
"UnableToLoadDownloadClients": "Kunne ikke indlæse downloadklienter",
|
||||
"UnableToLoadQualityDefinitions": "Kunne ikke indlæse kvalitetsdefinitioner",
|
||||
"UpdateCheckStartupTranslocationMessage": "Kan ikke installere opdatering, fordi startmappen '{0}' er i en App Translocation-mappe.",
|
||||
"UpdateMechanismHelpText": "Brug Radarrs indbyggede opdatering eller et script",
|
||||
"View": "Udsigt",
|
||||
"Warn": "Advare",
|
||||
"Movies": "Film",
|
||||
"AddingTag": "Tilføjer tag",
|
||||
"ApplicationStatusCheckAllClientMessage": "Alle lister er utilgængelige på grund af fejl",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Lister utilgængelige på grund af fejl: {0}",
|
||||
@@ -340,7 +301,6 @@
|
||||
"ClientPriority": "Kundens prioritet",
|
||||
"CloneProfile": "Klonprofil",
|
||||
"EnableInteractiveSearchHelpText": "Bruges, når der bruges interaktiv søgning",
|
||||
"EnableInteractiveSearchHelpTextWarning": "Søgning understøttes ikke med denne indekser",
|
||||
"UISettings": "UI-indstillinger",
|
||||
"NoUpdatesAreAvailable": "Ingen opdateringer er tilgængelige",
|
||||
"OAuthPopupMessage": "Pop-ups blokeres af din browser",
|
||||
@@ -357,9 +317,15 @@
|
||||
"AppDataDirectory": "AppData-bibliotek",
|
||||
"CertificateValidationHelpText": "Skift, hvor streng HTTPS-certificering er",
|
||||
"ChangeHasNotBeenSavedYet": "Ændring er endnu ikke gemt",
|
||||
"EnabledHelpText": "Aktivér denne liste til brug i Radarr",
|
||||
"ConnectSettings": "Forbind indstillinger",
|
||||
"DeleteBackup": "Slet sikkerhedskopi",
|
||||
"DeleteBackupMessageText": "Er du sikker på, at du vil slette sikkerhedskopien '{0}'?",
|
||||
"DeleteDownloadClient": "Slet Download Client"
|
||||
"DeleteDownloadClient": "Slet Download Client",
|
||||
"MaintenanceRelease": "Vedligeholdelsesfrigivelse: fejlrettelser og andre forbedringer. Se Github Commit History for flere detaljer",
|
||||
"Filters": "Filter",
|
||||
"HistoryCleanupDaysHelpText": "Sæt til 0 for at deaktivere automatisk oprydning",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Filer i papirkurven, der er ældre end det valgte antal dage, renses automatisk",
|
||||
"OnGrab": "On Grab",
|
||||
"OnHealthIssue": "Om sundhedsspørgsmål",
|
||||
"TestAllIndexers": "Test alle indeksører"
|
||||
}
|
||||
|
||||
@@ -6,16 +6,14 @@
|
||||
"Backup": "Backups",
|
||||
"BackupNow": "Jetzt sichern",
|
||||
"Clear": "Leeren",
|
||||
"Connect": "Verbindungen",
|
||||
"Connect": "Benachrichtigungen",
|
||||
"Connections": "Verbindungen",
|
||||
"CustomFilters": "Filter anpassen",
|
||||
"Date": "Datum",
|
||||
"Dates": "Termine",
|
||||
"Delete": "Löschen",
|
||||
"DownloadClientCheckNoneAvailableMessage": "Es ist kein Downloader verfügbar",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "Kommunikation mit {0} nicht möglich.",
|
||||
"DownloadClientStatusCheckAllClientMessage": "Alle Downloader sind aufgrund von Fehlern nicht verfügbar",
|
||||
"DownloadClientStatusCheckSingleClientMessage": "Downloader aufgrund von Fehlern nicht verfügbar: {0}",
|
||||
"DownloadClientStatusCheckAllClientMessage": "Alle Download Clients sind aufgrund von Fehlern nicht verfügbar",
|
||||
"DownloadClientStatusCheckSingleClientMessage": "Download Clients aufgrund von Fehlern nicht verfügbar: {0}",
|
||||
"DownloadClients": "Downloader",
|
||||
"Edit": "Bearbeiten",
|
||||
"Events": "Events",
|
||||
@@ -30,13 +28,9 @@
|
||||
"IndexerStatusCheckAllClientMessage": "Alle Indexer sind aufgrund von Fehlern nicht verfügbar",
|
||||
"IndexerStatusCheckSingleClientMessage": "Indexer aufgrund von Fehlern nicht verfügbar: {0}",
|
||||
"Indexers": "Indexer",
|
||||
"Languages": "Sprachen",
|
||||
"LogFiles": "Protokolle",
|
||||
"Logging": "Protokollierung",
|
||||
"MonoNotNetCoreCheckMessage": "Bitte aktualisieren Sie auf die .NET Core-Version von Prowlarr",
|
||||
"MonoTlsCheckMessage": "Der Workaround für Prowlarr Mono 4.x TLS ist weiterhin aktiviert. Entferne möglicherweise die Option MONO_TLS_PROVIDER=legacy",
|
||||
"MoreInfo": "Mehr Infos",
|
||||
"Movies": "Filme",
|
||||
"NoChange": "Keine Änderung",
|
||||
"NoChanges": "Keine Änderungen",
|
||||
"Options": "Optionen",
|
||||
@@ -45,13 +39,10 @@
|
||||
"ProxyCheckFailedToTestMessage": "Proxy konnte nicht getestet werden: {0}",
|
||||
"ProxyCheckResolveIpMessage": "Fehler beim Auflösen der IP-Adresse für den konfigurierten Proxy-Host {0}",
|
||||
"PtpOldSettingsCheckMessage": "Die folgenden PassThePopcorn-Indexer haben veraltete Einstellungen und sollten aktualisiert werden: {0}",
|
||||
"QualityDefinitions": "Qualitätsdefinitionen",
|
||||
"Queue": "Warteschlange",
|
||||
"Refresh": "Aktualisieren",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "Zweig {0} ist kein gültiger Prowlarr-Release-Zweig. Sie erhalten keine Updates",
|
||||
"ReleaseBranchCheckPreviousVersionMessage": "Zweig {0} ist für eine frühere Version von Prowlarr. Setzen Sie den Zweig für weitere Aktualisierungen auf 'nightly'",
|
||||
"RestoreBackup": "Backup einspielen",
|
||||
"Restrictions": "Beschränkungen",
|
||||
"SaveChanges": "Änderungen speichern",
|
||||
"Scheduled": "Geplant",
|
||||
"Search": "Suche",
|
||||
@@ -74,16 +65,16 @@
|
||||
"Updates": "Updates",
|
||||
"View": "Ansicht",
|
||||
"Language": "Sprache",
|
||||
"UISettingsSummary": "Einstellungen für Kalender, Datumsformat und Farbbeeinträchtigung",
|
||||
"UISettingsSummary": "Optionen für Datum, Sprache und Farbbeinträchtigungen",
|
||||
"TagsSettingsSummary": "Alle Tags und deren Benutzung anzeigen. Unbenutzte Tags können entfernt werden",
|
||||
"Size": "Größe",
|
||||
"ReleaseStatus": "Releasestatus",
|
||||
"Protocol": "Protokoll",
|
||||
"LastWriteTime": "Zuletzt beschrieben",
|
||||
"Indexer": "Indexer",
|
||||
"DownloadClientsSettingsSummary": "Downloader, Downloadverarbeitung und Remote-Pfadzuordnungen",
|
||||
"DownloadClientsSettingsSummary": "Download der Client-Konfigurationen für die Integration in die Prowlarr UI-Suche",
|
||||
"Grabbed": "Erfasste",
|
||||
"GeneralSettingsSummary": "Port, SSL, Benutzername/Passwort, Proxy, Statistik und Updates",
|
||||
"GeneralSettingsSummary": "Port, SSL, Benutzername/Passwort, Proxy, Analytik und Updates",
|
||||
"Filename": "Dateiname",
|
||||
"Failed": "Fehlgeschlagen",
|
||||
"EventType": "Event Typ",
|
||||
@@ -140,11 +131,8 @@
|
||||
"PendingChangesStayReview": "Auf der Seite bleiben",
|
||||
"PendingChangesMessage": "Es gibt noch ungespeicherte Änderungen, bist du sicher, dass du die Seite verlassen möchtest?",
|
||||
"PendingChangesDiscardChanges": "Änderungen verwerfen und schließen",
|
||||
"MonoVersionCheckUpgradeRecommendedMessage": "Die momentane installierte Mono Version {0} wird unterstützt aber es wird empfohlen auf {1} zu updaten.",
|
||||
"ExistingMovies": "Vorhandene Filme",
|
||||
"UpdateAutomaticallyHelpText": "Updates automatisch herunteraden und installieren. Es kann weiterhin unter \"System -> Updates\" ein manuelles Update angestoßen werden",
|
||||
"UrlBaseHelpText": "Für Reverse-Proxy-Unterstützung. Die Standardeinstellung leer",
|
||||
"EnableAutoHelpText": "Wenn aktiviert werden Filme dieser Liste automatisch hinzugefügt",
|
||||
"AppDataDirectory": "AppData Ordner",
|
||||
"ApplyTags": "Tags setzen",
|
||||
"Authentication": "Authentifizierung",
|
||||
@@ -163,17 +151,11 @@
|
||||
"CloneProfile": "Profil kopieren",
|
||||
"DeleteBackup": "Backup löschen",
|
||||
"DeleteDownloadClient": "Downloader löschen",
|
||||
"DeleteIndexer": "Indexer löschen",
|
||||
"DeleteTag": "Tag löschen",
|
||||
"Docker": "Docker",
|
||||
"DownloadClientSettings": "Downloader Einstellungen",
|
||||
"Enable": "Aktivieren",
|
||||
"EnableAutomaticAdd": "Automatisch hinzufügen",
|
||||
"EnableAutomaticSearch": "Automatisch suchen",
|
||||
"EnableColorImpairedMode": "Farbbeeinträchtigter Modus aktivieren",
|
||||
"EnableCompletedDownloadHandlingHelpText": "Importiere fertige Downloads vom Downloader automatisch",
|
||||
"EnabledHelpText": "Aktiviere diese Liste",
|
||||
"EnableHelpText": "Metadaten Dateien erstellen für diesen Metadata Typ",
|
||||
"EnableInteractiveSearch": "Interaktive Suche",
|
||||
"EnableSSL": "SSL",
|
||||
"EnableSslHelpText": " Erfordert einen Neustart als Administrator",
|
||||
@@ -182,21 +164,17 @@
|
||||
"Hostname": "Hostname",
|
||||
"IgnoredAddresses": "Ignorierte Adressen",
|
||||
"IllRestartLater": "Später neustarten",
|
||||
"Importing": "Importiere",
|
||||
"IncludeHealthWarningsHelpText": "Zustandswarnung",
|
||||
"IndexerFlags": "Indexer Flags",
|
||||
"IndexerFlags": "Indexer-Flags",
|
||||
"Interval": "Intervall",
|
||||
"LogLevel": "Log Level",
|
||||
"Logs": "Logs",
|
||||
"Logs": "Protokolle",
|
||||
"Mechanism": "Verfahren",
|
||||
"MIA": "MIA",
|
||||
"MinimumLimits": "Mindest Grenzen",
|
||||
"Mode": "Modus",
|
||||
"MonoVersion": "Mono Version",
|
||||
"NetCore": ".NET Core",
|
||||
"New": "Neu",
|
||||
"NoLeaveIt": "Nein, nicht ändern",
|
||||
"NoLimitForAnyRuntime": "Keine Begrenzung der Laufzeiten",
|
||||
"NotificationTriggers": "Benachrichtigungs Auslöser",
|
||||
"OnHealthIssueHelpText": "Zustandsproblem",
|
||||
"OpenBrowserOnStart": "Browser beim Start öffnen",
|
||||
@@ -205,11 +183,9 @@
|
||||
"Password": "Passwort",
|
||||
"Port": "Port",
|
||||
"PortNumber": "Port Nummer",
|
||||
"PreferredSize": "Bevorzugte Größe",
|
||||
"ProxyBypassFilterHelpText": "Verwende ',' als Trennzeichen und '*.' als Platzhalter für Subdomains",
|
||||
"ProxyType": "Proxy Typ",
|
||||
"ProxyUsernameHelpText": "Nur wenn ein Benutzername und Passwort erforderlich ist, muss es eingegeben werden. Ansonsten leer lassen.",
|
||||
"QualitySettings": "Qualitäts Einstellungen",
|
||||
"RefreshMovie": "Film aktualisieren",
|
||||
"RemovedFromTaskQueue": "Aus der Aufgabenwarteschlage entfernt",
|
||||
"RemoveFilter": "Filter entfernen",
|
||||
@@ -227,25 +203,20 @@
|
||||
"SuggestTranslationChange": "Schlage eine Übersetzung vor",
|
||||
"TagsHelpText": "Wird auf Filme mit mindestens einem passenden Tag angewandt",
|
||||
"TestAllClients": "Alle testen",
|
||||
"UpdateMechanismHelpText": "Benutze den Built-In Updater oder ein Script",
|
||||
"UpdateMechanismHelpText": "Benutze Prowlarr's Built-In Updater oder ein Script",
|
||||
"UpdateScriptPathHelpText": "Pfad zu einem benutzerdefinierten Skript, das ein extrahiertes Update-Paket übernimmt und den Rest des Update-Prozesses abwickelt",
|
||||
"Uptime": "Laufzeit",
|
||||
"URLBase": "URL Base",
|
||||
"URLBase": "URL-Basis",
|
||||
"Torrents": "Torrents",
|
||||
"UISettings": "Benutzeroberflächen Einstellungen",
|
||||
"UnableToLoadDownloadClients": "Downloader konnten nicht geladen werden",
|
||||
"UnableToLoadIndexers": "Indexer konnten nicht geladen werden",
|
||||
"UnableToLoadNotifications": "Benachrichtigungen konnten nicht geladen werden",
|
||||
"UnableToLoadQualityDefinitions": "Qualitätsdefinitionen konnten nicht geladen werden",
|
||||
"UnableToLoadTags": "Tags konnten nicht geladen werden",
|
||||
"Usenet": "Usenet",
|
||||
"UseProxy": "Proxy benutzen",
|
||||
"Username": "Benutzername",
|
||||
"Version": "Version",
|
||||
"YesCancel": "Ja, abbrechen",
|
||||
"EnableColorImpairedModeHelpText": "Alternativer Stil, um farbbeeinträchtigten Benutzern eine bessere Unterscheidung farbcodierter Informationen zu ermöglichen",
|
||||
"EnableMediaInfoHelpText": "Videoinformationen wie Auflösung, Laufzeit und Codec aus Datien erkennen. Dazu ist es erforderlich, dass Prowlarr Teile der Datei liest, was zu hoher Festplatten- oder Netzwerkaktivität während der Scans führen kann.",
|
||||
"NoMinimumForAnyRuntime": "Kein Minimum für Laufzeiten",
|
||||
"AnalyticsEnabledHelpText": "Sende anonyme Nutzungs- und Fehlerinformationen an die Server von Prowlarr. Dazu gehören Informationen über Browser, welche Seiten der Prowlarr-Weboberfläche aufgerufen wurden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.",
|
||||
"RestartRequiredHelpTextWarning": "Erfordert einen Neustart",
|
||||
"ApiKey": "API-Schlüssel",
|
||||
@@ -258,10 +229,7 @@
|
||||
"LaunchBrowserHelpText": " Öffne die Startseite von Prowlarr im Webbrowser nach dem Start.",
|
||||
"ReadTheWikiForMoreInformation": "Lese das Wiki für mehr Informationen",
|
||||
"BackupFolderHelpText": "Relative Pfade befinden sich unter Prowlarrs AppData Ordner",
|
||||
"DelayProfile": "Verzögerungsprofil",
|
||||
"MaximumLimits": "Maximale Grenzen",
|
||||
"DeleteNotification": "Benachrichtigung löschen",
|
||||
"CloneIndexer": "Indexer kopieren",
|
||||
"ConnectSettings": "Eintellungen für Verbindungen",
|
||||
"TagCannotBeDeletedWhileInUse": "Kann während der Benutzung nicht gelöscht werden",
|
||||
"SSLCertPathHelpText": "Pfad zur PFX Datei",
|
||||
@@ -269,20 +237,14 @@
|
||||
"ShownClickToHide": "Angezeigt, zum verstecken klicken",
|
||||
"RSSIsNotSupportedWithThisIndexer": "RSS wird von diesem Indexer nicht unterstützt",
|
||||
"RemovingTag": "Tag entfernen",
|
||||
"Pending": "Ausstehend",
|
||||
"Manual": "Manuell",
|
||||
"LogLevelTraceHelpTextWarning": "Trace logging sollte nur kurzzeitig aktiviert werden",
|
||||
"HiddenClickToShow": "Versteckt, klicken zum anzeigen",
|
||||
"ExistingTag": "Vorhandener Tag",
|
||||
"EnableInteractiveSearchHelpTextWarning": "Der Indexer unterstützt keine Suchen",
|
||||
"EnableInteractiveSearchHelpText": "Wird bei der manuellen Suche benutzt",
|
||||
"EnableAutomaticSearchHelpTextWarning": "Wird für die manuelle Suche benutzt",
|
||||
"EnableAutomaticSearchHelpText": "Wird für automatische Suchen genutzt die vom Benutzer oder von Prowlarr gestartet werden",
|
||||
"Downloading": "Lädt herunter",
|
||||
"DownloadClientUnavailable": "Downloader ist nicht verfügbar",
|
||||
"DeleteTagMessageText": "Tag '{0}' wirklich löschen?",
|
||||
"DeleteNotificationMessageText": "Benachrichtigung '{0}' wirklich löschen?",
|
||||
"DeleteIndexerMessageText": "Indexer '{0}' wirklich löschen?",
|
||||
"DeleteDownloadClientMessageText": "Downloader '{0}' wirklich löschen?",
|
||||
"DeleteBackupMessageText": "Backup '{0}' wirkich löschen?",
|
||||
"CancelPendingTask": "Diese laufende Aufgabe wirklich abbrechen?",
|
||||
@@ -306,10 +268,7 @@
|
||||
"NoTagsHaveBeenAddedYet": "Es wurden noch keine Tags erstellt",
|
||||
"NoLogFiles": "Keine Log-Dateien",
|
||||
"NoBackupsAreAvailable": "Es sind keine Backups vorhanden",
|
||||
"MinutesSixty": "60 Minuten: {0}",
|
||||
"MinutesNinety": "90 Minuten: {0}",
|
||||
"MinutesHundredTwenty": "120 Minuten: {0}",
|
||||
"MaintenanceRelease": "Wartungsupdate",
|
||||
"MaintenanceRelease": "Wartung: Fehlerbehebung und andere Verbesserungen. Siehe Github Commit History für weitere Details",
|
||||
"ForMoreInformationOnTheIndividualDownloadClients": "Für mehr Infomationen klicke auf die Info-Knöpfe.",
|
||||
"FilterPlaceHolder": "Filme suchen",
|
||||
"Exception": "Ausnahme",
|
||||
@@ -323,7 +282,7 @@
|
||||
"UILanguage": "Oberflächen Sprache ( UI Language )",
|
||||
"Priority": "Priorität",
|
||||
"InteractiveSearch": "Interaktive Suche",
|
||||
"IndexerPriorityHelpText": "Indexer Priorität von 1 (höchste) bis 50 (niedrigste). Standart: 25.",
|
||||
"IndexerPriorityHelpText": "Indexer Priorität von 1 (höchste) bis 50 (niedrigste). Standard: 25.",
|
||||
"IndexerPriority": "Priorität",
|
||||
"EditIndexer": "Indexer bearbeiten",
|
||||
"Disabled": "Deaktiviert",
|
||||
@@ -333,8 +292,6 @@
|
||||
"OpenThisModal": "Dieses Modal öffnen",
|
||||
"MovieIndexScrollTop": "Filmindex: Nach oben scrollen",
|
||||
"MovieIndexScrollBottom": "Filmindex: Nach unten scrollen",
|
||||
"MovieDetailsPreviousMovie": "Film Details: Vorheriger Film",
|
||||
"MovieDetailsNextMovie": "Film Details: Nächster Film",
|
||||
"FocusSearchBox": "Suchbox fokussieren",
|
||||
"CloseCurrentModal": "Momentanes Modal schließen",
|
||||
"AcceptConfirmationModal": "Bestätigung akzeptieren Modal",
|
||||
@@ -382,7 +339,7 @@
|
||||
"EditAppProfile": "App-Profil bearbeiten",
|
||||
"Wiki": "Wiki",
|
||||
"RSS": "RSS",
|
||||
"RedirectHelpText": "Leiten Sie eingehende Download-Anfragen für den Indexer um, statt Proxying mit Prowlarr",
|
||||
"RedirectHelpText": "Eingehende Download-Anfragen für den Indexer umleiten, anstatt Proxying mit Prowlarr",
|
||||
"Redirect": "Umleiten",
|
||||
"Reddit": "Reddit",
|
||||
"HomePage": "Startseite",
|
||||
@@ -396,8 +353,8 @@
|
||||
"UnableToLoadAppProfiles": "App-Profile können nicht geladen werden",
|
||||
"UnableToAddANewAppProfilePleaseTryAgain": "Es kann kein neues Anwendungsprofil hinzugefügt werden. Bitte versuchen Sie es erneut.",
|
||||
"UnableToAddANewApplicationPleaseTryAgain": "Es kann keine neue Anwendung hinzugefügt werden. Bitte versuchen Sie es erneut.",
|
||||
"SyncLevelFull": "Vollständige Synchronisierung: Hält diese App vollständig synchron. In Prowlarr vorgenommene Änderungen werden dann mit dieser App synchronisiert. Alle aus der Ferne vorgenommenen Änderungen werden von Prowlarr bei der nächsten Synchronisierung überschrieben.",
|
||||
"SyncLevelAddRemove": "Nur hinzufügen und entfernen: Wenn es in Prowlarr hinzugefügt oder entfernt wird, aktualisiert es diese Remote-App.",
|
||||
"SyncLevelFull": "Vollständige Synchronisierung: Hält die Indexer dieser App vollständig synchronisiert. Änderungen an Indexern in Prowlarr werden mit dieser App synchronisiert. Jede Änderung die and Indexern dieser App gemacht werden, wird von Prowlarr bei der nächsten Synchronisierung überschrieben.",
|
||||
"SyncLevelAddRemove": "Nur hinzufügen und entfernen: Wenn Indexer zu Prowlarr hinzugefügt oder entfernt werden, wird diese Remote-App aktualisiert.",
|
||||
"SyncLevel": "Sync-Level",
|
||||
"Privacy": "Privatsphäre",
|
||||
"Presets": "Voreinstellungen",
|
||||
@@ -429,5 +386,45 @@
|
||||
"AudioSearch": "Audio Suche",
|
||||
"AddIndexerProxy": "Indexer Proxy hinzufügen",
|
||||
"AppSettingsSummary": "Anwendungen und Einstellungen um zu konfigurieren, wie Prowlarr mit deinen PVR Programmen interagiert",
|
||||
"DeleteIndexerProxy": "Indexer Proxy löschen"
|
||||
"DeleteIndexerProxy": "Indexer Proxy löschen",
|
||||
"Notification": "Benachrichtigungen",
|
||||
"Filters": "Filter",
|
||||
"Notifications": "Benachrichtigungen",
|
||||
"HistoryCleanupDaysHelpText": "Auf 0 setzen um das automatische leeren des Papierkorbs zu deaktivieren",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Datien im Papierkorb die älter sind als der gewählte Wert, werden endgültig gelöscht",
|
||||
"OnApplicationUpdate": "Bei Anwendungsaktualisierung",
|
||||
"OnApplicationUpdateHelpText": "Bei Anwendungsaktualisierung",
|
||||
"OnGrab": "Bei Erfassung",
|
||||
"OnHealthIssue": "Bei Zustandsproblem",
|
||||
"TestAllIndexers": "Alle testen",
|
||||
"UserAgentProvidedByTheAppThatCalledTheAPI": "UserAgent von der App welcher die API aufgerufen hat",
|
||||
"BookSearch": "Buch Suche",
|
||||
"Id": "Id",
|
||||
"IndexerProxies": "Indexer-Proxies",
|
||||
"IndexerTagsHelpText": "Benutze Tags, um Indexer-Proxies zu spezifizieren oder um Indexer zu organisieren.",
|
||||
"MovieSearch": "Film Suche",
|
||||
"QueryOptions": "Abfrage-Optionen",
|
||||
"Categories": "Kategorien",
|
||||
"Database": "Datenbank",
|
||||
"IndexerNoDefCheckMessage": "Indexer haben keine Definition und werden nicht funktionieren: {0}. Bitte entferne und (oder) füge diese neu zu Prowlarr hinzu",
|
||||
"MassEditor": "Masseneditor",
|
||||
"Private": "Privat",
|
||||
"TvSearch": "TV Suche",
|
||||
"Url": "Url",
|
||||
"Auth": "Authentifizierung",
|
||||
"HistoryCleanup": "Verlaufsbereinigung",
|
||||
"IndexerAlreadySetup": "Mindestens eine Indexer Instanz ist bereits eingerichtet",
|
||||
"IndexerInfo": "Indexer-Info",
|
||||
"IndexerProxy": "Indexer-Proxy",
|
||||
"IndexerSettingsSummary": "Konfiguration verschiedener globaler Indexer Einstellungen, einschließlich Proxies.",
|
||||
"IndexerVipCheckExpiredClientMessage": "Die VIP Indexer Vorteile sind abgelaufen: {0}",
|
||||
"IndexerVipCheckExpiringClientMessage": "Die Indexer VIP Vorteile verfallen bald: {0}",
|
||||
"Proxies": "Proxies",
|
||||
"Public": "Öffentlich",
|
||||
"QueryResults": "Abfrageergebnisse",
|
||||
"SearchType": "Suchtyp",
|
||||
"SemiPrivate": "Halbprivat",
|
||||
"UnableToLoadApplicationList": "Anwendungsliste kann nicht geladen werden",
|
||||
"UnableToLoadIndexerProxies": "Indexer-Proxies können nicht geladen werden",
|
||||
"Website": "Webseite"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"DownloadClients": "Προγράμματα Λήψης",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "Αδύνατο να επικοινωνήσει με {0}.",
|
||||
"DownloadClientCheckNoneAvailableMessage": "Δεν υπάρχει διαθέσιμο πρόγραμμα Λήψης",
|
||||
"DownloadClient": "Πρόγραμμα Λήψης",
|
||||
"Details": "Λεπτομέρειες",
|
||||
"Delete": "Διαγραφή",
|
||||
@@ -61,7 +59,6 @@
|
||||
"IllRestartLater": "Θα επανεκκινήσω αργότερα",
|
||||
"Protocol": "Πρωτόκολλο",
|
||||
"Reddit": "Reddit",
|
||||
"Restrictions": "Περιορισμοί",
|
||||
"Result": "Αποτέλεσμα",
|
||||
"Retention": "Κράτηση",
|
||||
"RSS": "RSS",
|
||||
@@ -77,16 +74,13 @@
|
||||
"ApplyTagsHelpTexts2": "Προσθήκη: Προσθέστε τις ετικέτες στην υπάρχουσα λίστα ετικετών",
|
||||
"ApplyTagsHelpTexts3": "Αφαίρεση: Καταργήστε τις καταχωρημένες ετικέτες",
|
||||
"AreYouSureYouWantToResetYourAPIKey": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε το κλειδί API σας;",
|
||||
"DeleteIndexer": "Διαγραφή ευρετηρίου",
|
||||
"NoChange": "Καμία αλλαγή",
|
||||
"Port": "Λιμάνι",
|
||||
"PortNumber": "Αριθμός θύρας",
|
||||
"PreferredSize": "Προτιμώμενο μέγεθος",
|
||||
"IndexerStatusCheckAllClientMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών",
|
||||
"IndexerStatusCheckSingleClientMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών: {0}",
|
||||
"KeyboardShortcuts": "Συντομεύσεις πληκτρολογίου",
|
||||
"Language": "Γλώσσα",
|
||||
"MonoTlsCheckMessage": "Η λύση Radarr Mono 4.x tls είναι ακόμα ενεργοποιημένη, εξετάστε το ενδεχόμενο να καταργήσετε το MONO_TLS_PROVIDER = επιλογή περιβάλλοντος παλαιού τύπου",
|
||||
"PriorityHelpText": "Προτεραιότητα πολλαπλών πελατών λήψης. Το Round-Robin χρησιμοποιείται για πελάτες με την ίδια προτεραιότητα.",
|
||||
"PrioritySettings": "Προτεραιότητα",
|
||||
"Reset": "Επαναφορά",
|
||||
@@ -103,11 +97,6 @@
|
||||
"ConnectSettings": "Σύνδεση ρυθμίσεων",
|
||||
"Disabled": "άτομα με ειδικές ανάγκες",
|
||||
"Manual": "Εγχειρίδιο",
|
||||
"MaximumLimits": "Μέγιστα όρια",
|
||||
"MinutesSixty": "60 λεπτά: {0}",
|
||||
"MovieDetailsNextMovie": "Λεπτομέρειες ταινίας: Επόμενη ταινία",
|
||||
"QualityDefinitions": "Ορισμοί ποιότητας",
|
||||
"QualitySettings": "Ρυθμίσεις ποιότητας",
|
||||
"SetTags": "Ορισμός ετικετών",
|
||||
"ShowSearch": "Εμφάνιση αναζήτησης",
|
||||
"Status": "Κατάσταση",
|
||||
@@ -123,7 +112,6 @@
|
||||
"IndexerPriority": "Προτεραιότητα ευρετηρίου",
|
||||
"Indexers": "Ευρετήρια",
|
||||
"Info": "Πληροφορίες",
|
||||
"Languages": "Γλώσσες",
|
||||
"LastWriteTime": "Τελευταία ώρα εγγραφής",
|
||||
"Level": "Επίπεδο",
|
||||
"LogLevel": "Επίπεδο καταγραφής",
|
||||
@@ -143,24 +131,16 @@
|
||||
"CloneProfile": "Προφίλ κλώνου",
|
||||
"CloseCurrentModal": "Κλείσιμο τρέχοντος modal",
|
||||
"DBMigration": "Μετεγκατάσταση DB",
|
||||
"DelayProfile": "Προφίλ χρονοκαθυστέρησης",
|
||||
"DeleteApplicationMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε την ειδοποίηση \"{0}\";",
|
||||
"DeleteBackupMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αντίγραφο ασφαλείας \"{0}\";",
|
||||
"DeleteIndexerMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το ευρετήριο \"{0}\";",
|
||||
"DeleteIndexerProxyMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη λίστα \"{0}\";",
|
||||
"DeleteNotificationMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε την ειδοποίηση \"{0}\";",
|
||||
"DeleteTagMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε την ετικέτα \"{0}\";",
|
||||
"DownloadClientUnavailable": "Ο πελάτης λήψης δεν είναι διαθέσιμος",
|
||||
"EnableAutoHelpText": "Εάν ενεργοποιηθεί, οι Ταινίες θα προστεθούν αυτόματα στο Radarr από αυτήν τη λίστα",
|
||||
"EnableAutomaticAdd": "Ενεργοποίηση αυτόματης προσθήκης",
|
||||
"EnableAutomaticSearch": "Ενεργοποίηση αυτόματης αναζήτησης",
|
||||
"EnableAutomaticSearchHelpText": "Θα χρησιμοποιηθεί όταν πραγματοποιούνται αυτόματες αναζητήσεις μέσω του περιβάλλοντος χρήστη ή του Radarr",
|
||||
"EnableColorImpairedModeHelpText": "Τροποποιημένο στυλ για να επιτρέπεται στους χρήστες με προβλήματα χρώματος να διακρίνουν καλύτερα τις πληροφορίες με χρωματική κωδικοποίηση",
|
||||
"EnableMediaInfoHelpText": "Εξαγωγή πληροφοριών βίντεο, όπως ανάλυση, χρόνος εκτέλεσης και πληροφορίες κωδικοποιητή από αρχεία. Αυτό απαιτεί από τον Radarr να διαβάσει τμήματα του αρχείου που ενδέχεται να προκαλέσουν υψηλή δραστηριότητα δίσκου ή δικτύου κατά τη διάρκεια των σαρώσεων.",
|
||||
"EnableSslHelpText": " Απαιτείται επανεκκίνηση ως διαχειριστής για να τεθεί σε ισχύ",
|
||||
"Error": "Λάθος",
|
||||
"ErrorLoadingContents": "Σφάλμα κατά τη φόρτωση περιεχομένων",
|
||||
"ExistingMovies": "Υφιστάμενες ταινίες",
|
||||
"GeneralSettings": "Γενικές Ρυθμίσεις",
|
||||
"Grabs": "Αρπάζω",
|
||||
"HealthNoIssues": "Δεν υπάρχουν προβλήματα με τη διαμόρφωσή σας",
|
||||
@@ -177,29 +157,19 @@
|
||||
"LogLevelTraceHelpTextWarning": "Η καταγραφή ιχνών πρέπει να ενεργοποιηθεί προσωρινά",
|
||||
"Mechanism": "Μηχανισμός",
|
||||
"Message": "Μήνυμα",
|
||||
"MinimumLimits": "Ελάχιστα όρια",
|
||||
"MinutesHundredTwenty": "120 λεπτά: {0}",
|
||||
"MinutesNinety": "90 λεπτά: {0}",
|
||||
"MonoVersion": "Μονο έκδοση",
|
||||
"MonoVersionCheckUpgradeRecommendedMessage": "Η τρέχουσα εγκατεστημένη έκδοση Mono {0} υποστηρίζεται, αλλά συνιστάται η αναβάθμιση σε {1}.",
|
||||
"MovieDetailsPreviousMovie": "Λεπτομέρειες ταινίας: Προηγούμενη ταινία",
|
||||
"MovieIndexScrollBottom": "Ευρετήριο ταινιών: Κύλιση κάτω",
|
||||
"MovieIndexScrollTop": "Ευρετήριο ταινιών: Κύλιση στην κορυφή",
|
||||
"Movies": "Κινηματογράφος",
|
||||
"Name": "Ονομα",
|
||||
"New": "Νέος",
|
||||
"NoBackupsAreAvailable": "Δεν υπάρχουν διαθέσιμα αντίγραφα ασφαλείας",
|
||||
"NoLeaveIt": "Όχι, άσε το",
|
||||
"NoLimitForAnyRuntime": "Δεν υπάρχει όριο για οποιοδήποτε χρόνο εκτέλεσης",
|
||||
"NoLinks": "Χωρίς συνδέσμους",
|
||||
"NoLogFiles": "Δεν υπάρχουν αρχεία καταγραφής",
|
||||
"NoMinimumForAnyRuntime": "Χωρίς ελάχιστο για κάθε χρόνο εκτέλεσης",
|
||||
"NoTagsHaveBeenAddedYet": "Δεν έχουν προστεθεί ετικέτες ακόμη",
|
||||
"NotificationTriggers": "Ενεργοποιήσεις ειδοποίησης",
|
||||
"PageSizeHelpText": "Αριθμός στοιχείων προς εμφάνιση σε κάθε σελίδα",
|
||||
"Password": "Κωδικός πρόσβασης",
|
||||
"Peers": "Ομότιμοι",
|
||||
"Pending": "εκκρεμής",
|
||||
"PendingChangesMessage": "Έχετε μη αποθηκευμένες αλλαγές, είστε βέβαιοι ότι θέλετε να αποχωρήσετε από αυτήν τη σελίδα;",
|
||||
"PendingChangesStayReview": "Παραμείνετε και ελέγξτε τις αλλαγές",
|
||||
"Presets": "Προεπιλογές",
|
||||
@@ -257,7 +227,6 @@
|
||||
"UnableToAddANewIndexerPleaseTryAgain": "Δεν είναι δυνατή η προσθήκη νέου ευρετηρίου, δοκιμάστε ξανά.",
|
||||
"UnableToLoadGeneralSettings": "Δεν είναι δυνατή η φόρτωση των γενικών ρυθμίσεων",
|
||||
"UnableToLoadNotifications": "Δεν είναι δυνατή η φόρτωση ειδοποιήσεων",
|
||||
"UnableToLoadQualityDefinitions": "Δεν είναι δυνατή η φόρτωση των ορισμών ποιότητας",
|
||||
"UnableToLoadUISettings": "Δεν είναι δυνατή η φόρτωση των ρυθμίσεων διεπαφής χρήστη",
|
||||
"UnsavedChanges": "Μη αποθηκευμένες αλλαγές",
|
||||
"UpdateCheckStartupTranslocationMessage": "Δεν είναι δυνατή η εγκατάσταση της ενημέρωσης επειδή ο φάκελος εκκίνησης \"{0}\" βρίσκεται σε ένα φάκελο \"Μετατόπιση εφαρμογών\".",
|
||||
@@ -272,19 +241,16 @@
|
||||
"Warn": "Προειδοποιώ",
|
||||
"YesCancel": "Ναι, Ακύρωση",
|
||||
"Exception": "Εξαίρεση",
|
||||
"Importing": "Εισαγωγή",
|
||||
"IncludeHealthWarningsHelpText": "Συμπεριλάβετε προειδοποιήσεις για την υγεία",
|
||||
"Security": "Ασφάλεια",
|
||||
"Tasks": "Καθήκοντα",
|
||||
"UnableToLoadBackups": "Δεν είναι δυνατή η φόρτωση αντιγράφων ασφαλείας",
|
||||
"UnableToLoadDownloadClients": "Δεν είναι δυνατή η φόρτωση πελατών λήψης",
|
||||
"UnableToLoadIndexers": "Δεν είναι δυνατή η φόρτωση του ευρετηρίου",
|
||||
"UpdateMechanismHelpText": "Χρησιμοποιήστε το ενσωματωμένο πρόγραμμα ενημέρωσης του Radarr ή ένα σενάριο",
|
||||
"AnalyticsEnabledHelpText": "Στείλτε ανώνυμες πληροφορίες χρήσης και σφάλματος στους διακομιστές του Radarr. Αυτό περιλαμβάνει πληροφορίες στο πρόγραμμα περιήγησής σας, ποιες σελίδες Radarr WebUI χρησιμοποιείτε, αναφορά σφαλμάτων καθώς και έκδοση λειτουργικού συστήματος και χρόνου εκτέλεσης. Θα χρησιμοποιήσουμε αυτές τις πληροφορίες για να δώσουμε προτεραιότητα σε λειτουργίες και διορθώσεις σφαλμάτων.",
|
||||
"AppDataDirectory": "Κατάλογος AppData",
|
||||
"BindAddress": "Δεσμευμένη διεύθυνση",
|
||||
"EnableRss": "Ενεργοποίηση RSS",
|
||||
"EnableInteractiveSearchHelpTextWarning": "Η αναζήτηση δεν υποστηρίζεται με αυτό το ευρετήριο",
|
||||
"IndexerFlags": "Σημαίες ευρετηρίου",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών για περισσότερο από 6 ώρες",
|
||||
"InteractiveSearch": "Διαδραστική αναζήτηση",
|
||||
@@ -314,14 +280,12 @@
|
||||
"CertificateValidation": "Επικύρωση πιστοποιητικού",
|
||||
"CertificateValidationHelpText": "Αλλάξτε πόσο αυστηρή είναι η επικύρωση πιστοποίησης HTTPS",
|
||||
"ClientPriority": "Προτεραιότητα πελάτη",
|
||||
"CloneIndexer": "Δείκτης κλώνου",
|
||||
"DeleteBackup": "Διαγραφή αντιγράφων ασφαλείας",
|
||||
"DeleteDownloadClient": "Διαγραφή προγράμματος-πελάτη λήψης",
|
||||
"DeleteDownloadClientMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον πελάτη λήψης \"{0}\";",
|
||||
"DeleteNotification": "Διαγραφή ειδοποίησης",
|
||||
"Docker": "Λιμενεργάτης",
|
||||
"DownloadClientSettings": "Λήψη ρυθμίσεων πελάτη",
|
||||
"Downloading": "Λήψη",
|
||||
"EnableInteractiveSearch": "Ενεργοποίηση διαδραστικής αναζήτησης",
|
||||
"SuggestTranslationChange": "Προτείνετε αλλαγή μετάφρασης",
|
||||
"System": "Σύστημα",
|
||||
@@ -354,11 +318,14 @@
|
||||
"DeleteTag": "Διαγραφή ετικέτας",
|
||||
"EditIndexer": "Επεξεργασία ευρετηρίου",
|
||||
"Enable": "επιτρέπω",
|
||||
"EnableAutomaticSearchHelpTextWarning": "Θα χρησιμοποιηθεί όταν χρησιμοποιείται διαδραστική αναζήτηση",
|
||||
"EnableColorImpairedMode": "Ενεργοποίηση λειτουργίας με προβλήματα χρώματος",
|
||||
"EnableCompletedDownloadHandlingHelpText": "Αυτόματη εισαγωγή ολοκληρωμένων λήψεων από τον πελάτη λήψης",
|
||||
"Enabled": "Ενεργοποιήθηκε",
|
||||
"EnabledHelpText": "Ενεργοποιήστε αυτήν τη λίστα για χρήση στο Radarr",
|
||||
"EnableHelpText": "Ενεργοποίηση δημιουργίας αρχείων μεταδεδομένων για αυτόν τον τύπο μεταδεδομένων",
|
||||
"EnableInteractiveSearchHelpText": "Θα χρησιμοποιηθεί όταν χρησιμοποιείται διαδραστική αναζήτηση"
|
||||
"EnableInteractiveSearchHelpText": "Θα χρησιμοποιηθεί όταν χρησιμοποιείται διαδραστική αναζήτηση",
|
||||
"OnGrab": "Στο Grab",
|
||||
"Filters": "Φίλτρο",
|
||||
"HistoryCleanupDaysHelpText": "Ορίστε σε 0 για να απενεργοποιήσετε τον αυτόματο καθαρισμό",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Τα αρχεία στον κάδο ανακύκλωσης παλαιότερα από τον επιλεγμένο αριθμό ημερών θα καθαρίζονται αυτόματα",
|
||||
"OnHealthIssue": "Σχετικά με το θέμα της υγείας",
|
||||
"TestAllIndexers": "Δοκιμάστε όλους τους δείκτες",
|
||||
"MaintenanceRelease": "Έκδοση συντήρησης: επιδιορθώσεις σφαλμάτων και άλλες βελτιώσεις. Δείτε το Github Commit History για περισσότερες λεπτομέρειες",
|
||||
"ConnectionLostMessage": "Το Radarr έχασε τη σύνδεσή του με το backend και θα χρειαστεί να επαναφορτωθεί για να αποκαταστήσει τη λειτουργικότητά του."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user