mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-17 21:26:22 -04:00
Compare commits
143 Commits
movie-deta
...
v4.2.1.647
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e72f8097fb | ||
|
|
3eec088306 | ||
|
|
ad097dd1a2 | ||
|
|
b4b38a5318 | ||
|
|
b0717a0803 | ||
|
|
4d1d08d345 | ||
|
|
e689817508 | ||
|
|
3b191caf16 | ||
|
|
cc6ca0b067 | ||
|
|
57cb63fb18 | ||
|
|
20f709d22a | ||
|
|
5d8775ac96 | ||
|
|
4890972e16 | ||
|
|
40dc808f61 | ||
|
|
97077e09d2 | ||
|
|
9ba7027d00 | ||
|
|
9903e70925 | ||
|
|
3a6f3666f5 | ||
|
|
915c66be50 | ||
|
|
70b22e483a | ||
|
|
cad1191da5 | ||
|
|
43910af127 | ||
|
|
f01c477b81 | ||
|
|
0054318658 | ||
|
|
03a3f4522a | ||
|
|
3d3562dcda | ||
|
|
7a079c5e0c | ||
|
|
4d70798f2f | ||
|
|
d55864f869 | ||
|
|
3c41c84fb0 | ||
|
|
eae9a6d6e0 | ||
|
|
867f8f5835 | ||
|
|
0c81387cfb | ||
|
|
c5fb5200de | ||
|
|
cc306fcd36 | ||
|
|
2bb7984961 | ||
|
|
21e605452a | ||
|
|
476f5b5bfd | ||
|
|
b6920cfe82 | ||
|
|
e89b98d0f6 | ||
|
|
1db690ad39 | ||
|
|
d5c524719b | ||
|
|
ced6586860 | ||
|
|
8b3019821a | ||
|
|
16ed68d5de | ||
|
|
098a893083 | ||
|
|
548e3400b5 | ||
|
|
5c31e3f1a2 | ||
|
|
7404793dcf | ||
|
|
d8af17ce3d | ||
|
|
44c912f02d | ||
|
|
b104368e23 | ||
|
|
aa0104b6bc | ||
|
|
69fcd8ec94 | ||
|
|
a59928c66a | ||
|
|
1cb7ae11a2 | ||
|
|
ca519047dd | ||
|
|
f15a6abde0 | ||
|
|
2aacebc938 | ||
|
|
120e9b673e | ||
|
|
0a77a13fa8 | ||
|
|
383f9647c3 | ||
|
|
7f7c672b93 | ||
|
|
2690ad8fe1 | ||
|
|
801204b6de | ||
|
|
cb9514abaf | ||
|
|
fd22cb44f6 | ||
|
|
2d68716376 | ||
|
|
b97e76c8b8 | ||
|
|
bfad4a8cd1 | ||
|
|
61f05710f5 | ||
|
|
a8ecefd91f | ||
|
|
e3468daba0 | ||
|
|
f2a7d0d520 | ||
|
|
43257f0726 | ||
|
|
6c2bf860fe | ||
|
|
3a1d848e59 | ||
|
|
f6590e71d2 | ||
|
|
586dd737fd | ||
|
|
fa84dda38c | ||
|
|
4a233ce915 | ||
|
|
ffdd9a1708 | ||
|
|
f890aadffa | ||
|
|
a8695959f1 | ||
|
|
696bb845a5 | ||
|
|
301a6904c0 | ||
|
|
8b4621db61 | ||
|
|
822b597f26 | ||
|
|
737a0176d4 | ||
|
|
5dc541c69e | ||
|
|
a9627771e6 | ||
|
|
5facab0744 | ||
|
|
63dde3eb89 | ||
|
|
144f564076 | ||
|
|
a6205c1ad4 | ||
|
|
64f27bca4f | ||
|
|
8a84975954 | ||
|
|
e923b2fc6c | ||
|
|
a4136150d0 | ||
|
|
1f1f3cdaa2 | ||
|
|
bd85936a62 | ||
|
|
93b2395228 | ||
|
|
14cccd3a23 | ||
|
|
006c9289de | ||
|
|
79cd2b2346 | ||
|
|
f80272a659 | ||
|
|
a158e008e9 | ||
|
|
c798987379 | ||
|
|
097b46c49f | ||
|
|
ddeb3a7840 | ||
|
|
50018d0325 | ||
|
|
a37fc0dc1f | ||
|
|
39ad315e73 | ||
|
|
0559996566 | ||
|
|
e7d4429fe4 | ||
|
|
6c494e9a92 | ||
|
|
62faa1aad8 | ||
|
|
907950e309 | ||
|
|
1caced614e | ||
|
|
5824ba963b | ||
|
|
7f2d5d8d10 | ||
|
|
81bffe243a | ||
|
|
2d6fde282a | ||
|
|
3125b038d5 | ||
|
|
89e25a6241 | ||
|
|
4db6688fe0 | ||
|
|
4ac1aeaf06 | ||
|
|
e2ae743ee1 | ||
|
|
9ad316a6f5 | ||
|
|
b643d2e23d | ||
|
|
6a03eddda9 | ||
|
|
1576bf1f17 | ||
|
|
6325b70e27 | ||
|
|
24206ad0a3 | ||
|
|
2fc7cbff89 | ||
|
|
55ef505d74 | ||
|
|
cabdad6306 | ||
|
|
8d4b2dd21b | ||
|
|
ad04031c99 | ||
|
|
e9a5f87e45 | ||
|
|
bc6ac0cd4b | ||
|
|
c2328e4b79 | ||
|
|
36119facf0 |
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"paths": [
|
||||
"frontend/src/**/*.js",
|
||||
"src/NzbDrone.Core/Localization/Core/*.json"
|
||||
"frontend/src/**/*.js"
|
||||
],
|
||||
"ignored": [
|
||||
"**/node_modules/**/*"
|
||||
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.1.0'
|
||||
majorVersion: '4.2.1'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.201'
|
||||
dotnetVersion: '6.0.400'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
@@ -97,15 +97,14 @@ stages:
|
||||
- bash: |
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
grep osx-x64 $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "BSD already enabled"
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling BSD support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Enable FreeBSD Support
|
||||
- bash: ./build.sh --backend --enable-bsd
|
||||
displayName: Enable Extra Platform Support
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
displayName: Build Radarr Backend
|
||||
- bash: |
|
||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
@@ -119,24 +118,28 @@ stages:
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
||||
- stage: Build_Frontend
|
||||
@@ -170,7 +173,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
@@ -240,7 +242,7 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages --enable-bsd
|
||||
- bash: ./build.sh --packages --enable-extra-platforms
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
find . -name "ffprobe" -exec chmod a+x {} \;
|
||||
@@ -248,28 +250,28 @@ stages:
|
||||
find . -name "Radarr.Update" -exec chmod a+x {} \;
|
||||
displayName: Set executable bits
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows Core zip
|
||||
displayName: Create win-x64 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows x86 Core zip
|
||||
displayName: Create win-x86 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS x64 Core app
|
||||
displayName: Create osx-x64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS x64 Core tar
|
||||
displayName: Create osx-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -277,14 +279,14 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core app
|
||||
displayName: Create osx-arm64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core tar
|
||||
displayName: Create osx-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -292,7 +294,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
displayName: Create linux-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -300,7 +302,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
displayName: Create linux-musl-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -308,7 +310,15 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
displayName: Create linux-x86 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x86.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -316,7 +326,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -324,7 +334,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Core tar
|
||||
displayName: Create linux-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -332,7 +342,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -340,7 +350,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
displayName: Create freebsd-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -407,22 +417,22 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
testName: 'osx-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.macImage }}
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
testName: 'win-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
testName: 'linux-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
FreebsdCore:
|
||||
osName: 'Linux'
|
||||
testName: 'FreebsdCore'
|
||||
testName: 'freebsd-x64'
|
||||
poolName: 'FreeBSD'
|
||||
imageName:
|
||||
|
||||
@@ -441,7 +451,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
@@ -475,8 +485,12 @@ stages:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
@@ -487,9 +501,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -522,14 +542,14 @@ stages:
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: LinuxCoreTests
|
||||
artifactName: linux-x64-tests
|
||||
Radarr__Postgres__Host: 'localhost'
|
||||
Radarr__Postgres__Port: '5432'
|
||||
Radarr__Postgres__User: 'radarr'
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
@@ -556,6 +576,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=radarr \
|
||||
-e POSTGRES_USER=radarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -597,17 +618,17 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
testName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
testName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
testName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
|
||||
@@ -624,7 +645,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -666,7 +687,7 @@ stages:
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
@@ -678,7 +699,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'LinuxCoreTests'
|
||||
artifactName: 'linux-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -701,6 +722,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=radarr \
|
||||
-e POSTGRES_USER=radarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -732,7 +754,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'FreebsdCoreTests'
|
||||
artifactName: 'freebsd-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -768,10 +790,15 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
testName: 'linux-musl-x64'
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
pattern: 'Radarr.*.linux-core-x86.tar.gz'
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
@@ -781,9 +808,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -829,16 +862,19 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
artifactName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
artifactName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
artifactName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||
failBuild: true
|
||||
@@ -856,7 +892,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(osName)CoreTests'
|
||||
artifactName: '$(artifactName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -941,7 +977,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
|
||||
44
build.sh
44
build.sh
@@ -25,14 +25,22 @@ UpdateVersionNumber()
|
||||
fi
|
||||
}
|
||||
|
||||
EnableBsdSupport()
|
||||
EnableExtraPlatformsInSDK()
|
||||
{
|
||||
#todo enable sdk with
|
||||
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
}
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -292,7 +300,8 @@ if [ $# -eq 0 ]; then
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_BSD=NO
|
||||
ENABLE_EXTRA_PLATFORMS=NO
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
@@ -304,8 +313,12 @@ case $key in
|
||||
BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-bsd)
|
||||
ENABLE_BSD=YES
|
||||
--enable-bsd|--enable-extra-platforms)
|
||||
ENABLE_EXTRA_PLATFORMS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-extra-platforms-in-sdk)
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
@@ -349,12 +362,17 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatformsInSDK
|
||||
fi
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
EnableBsdSupport
|
||||
EnableExtraPlatforms
|
||||
fi
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
@@ -364,9 +382,10 @@ then
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -405,9 +424,10 @@ then
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
|
||||
@@ -179,6 +179,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
@@ -190,7 +190,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
|
||||
<div>
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
iconSize={13}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.movie {
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -6,3 +7,19 @@
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
flex: 1 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tmdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.tmdbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ImportMovieTitle from './ImportMovieTitle';
|
||||
import styles from './ImportMovieSearchResult.css';
|
||||
|
||||
@@ -18,6 +20,7 @@ class ImportMovieSearchResult extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
title,
|
||||
year,
|
||||
studio,
|
||||
@@ -25,17 +28,30 @@ class ImportMovieSearchResult extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.movie}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={studio}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</Link>
|
||||
<div className={styles.container}>
|
||||
<Link
|
||||
className={styles.movie}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={studio}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={styles.tmdbLink}
|
||||
to={`https://www.themoviedb.org/movie/${tmdbId}`}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tmdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={16}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Radarr">
|
||||
<DocumentTitle title={window.Radarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
|
||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
@@ -22,6 +25,9 @@ class CollectionFooter extends Component {
|
||||
this.state = {
|
||||
monitor: NO_CHANGE,
|
||||
monitored: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
destinationRootFolder: null
|
||||
};
|
||||
}
|
||||
@@ -36,7 +42,10 @@ class CollectionFooter extends Component {
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
monitor: NO_CHANGE
|
||||
monitor: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,7 +64,10 @@ class CollectionFooter extends Component {
|
||||
onUpdateSelectedPress = () => {
|
||||
const {
|
||||
monitor,
|
||||
monitored
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
rootFolderPath
|
||||
} = this.state;
|
||||
|
||||
const changes = {};
|
||||
@@ -68,6 +80,18 @@ class CollectionFooter extends Component {
|
||||
changes.monitor = monitor;
|
||||
}
|
||||
|
||||
if (qualityProfileId !== NO_CHANGE) {
|
||||
changes.qualityProfileId = qualityProfileId;
|
||||
}
|
||||
|
||||
if (minimumAvailability !== NO_CHANGE) {
|
||||
changes.minimumAvailability = minimumAvailability;
|
||||
}
|
||||
|
||||
if (rootFolderPath !== NO_CHANGE) {
|
||||
changes.rootFolderPath = rootFolderPath;
|
||||
}
|
||||
|
||||
this.props.onUpdateSelectedPress(changes);
|
||||
};
|
||||
|
||||
@@ -82,11 +106,14 @@ class CollectionFooter extends Component {
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitor
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
rootFolderPath
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: NO_CHANGE, value: translate('No Change'), disabled: true },
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'monitored', value: translate('Monitored') },
|
||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||
];
|
||||
@@ -125,6 +152,52 @@ class CollectionFooter extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('MinimumAvailability')}
|
||||
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<AvailabilitySelectInput
|
||||
name="minimumAvailability"
|
||||
value={minimumAvailability}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<CollectionFooterLabel
|
||||
|
||||
50
frontend/src/Collection/Overview/CollectionMovieLabel.css
Normal file
50
frontend/src/Collection/Overview/CollectionMovieLabel.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.movie {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
margin: 2px 4px;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: #eee;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.movieTitle {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.movieStatus {
|
||||
padding: 0 4px;
|
||||
border-left: 4px;
|
||||
border-left-style: solid;
|
||||
background-color: $white;
|
||||
color: $defaultColor;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border-color: $primaryColor;
|
||||
}
|
||||
|
||||
.danger {
|
||||
border-color: $dangerColor;
|
||||
}
|
||||
|
||||
.success {
|
||||
border-color: $successColor;
|
||||
}
|
||||
|
||||
.purple {
|
||||
border-color: $purple;
|
||||
}
|
||||
|
||||
.warning {
|
||||
border-color: $warningColor;
|
||||
}
|
||||
|
||||
.info {
|
||||
border-color: $infoColor;
|
||||
}
|
||||
|
||||
.queue {
|
||||
border-color: $queueColor;
|
||||
}
|
||||
84
frontend/src/Collection/Overview/CollectionMovieLabel.js
Normal file
84
frontend/src/Collection/Overview/CollectionMovieLabel.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './CollectionMovieLabel.css';
|
||||
|
||||
class CollectionMovieLabel extends Component {
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
status,
|
||||
monitored,
|
||||
isAvailable,
|
||||
hasFile,
|
||||
onMonitorTogglePress,
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.movie}>
|
||||
<div className={styles.movieTitle}>
|
||||
{
|
||||
id &&
|
||||
<MonitorToggleButton
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
<span>
|
||||
{
|
||||
title
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
id &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.movieStatus,
|
||||
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
|
||||
)}
|
||||
>
|
||||
{
|
||||
hasFile ? translate('Downloaded') : translate('Missing')
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionMovieLabel.propTypes = {
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string,
|
||||
isAvailable: PropTypes.bool,
|
||||
monitored: PropTypes.bool,
|
||||
hasFile: PropTypes.bool,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
movieFile: PropTypes.object,
|
||||
movieFileId: PropTypes.number,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
CollectionMovieLabel.defaultProps = {
|
||||
isSaving: false,
|
||||
statistics: {
|
||||
episodeFileCount: 0,
|
||||
totalEpisodeCount: 0,
|
||||
percentOfEpisodes: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default CollectionMovieLabel;
|
||||
@@ -0,0 +1,59 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import CollectionMovieLabel from './CollectionMovieLabel';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDimensionsSelector(),
|
||||
createCollectionExistingMovieSelector(),
|
||||
(dimensions, existingMovie) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
isExistingMovie: !!existingMovie,
|
||||
...existingMovie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleMovieMonitored
|
||||
};
|
||||
|
||||
class CollectionMovieLabelConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMonitorTogglePress = (monitored) => {
|
||||
this.props.toggleMovieMonitored({
|
||||
movieId: this.props.id,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollectionMovieLabel
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionMovieLabelConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool,
|
||||
toggleMovieMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieLabelConnector);
|
||||
@@ -32,6 +32,11 @@ $hoverScale: 1.05;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.labelsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.moviesContainer {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Slider from 'react-slick';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector';
|
||||
import Carousel from 'Components/Carousel';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
@@ -14,8 +14,12 @@ import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CollectionMovieConnector from './CollectionMovieConnector';
|
||||
import CollectionMovieLabelConnector from './CollectionMovieLabelConnector';
|
||||
import styles from './CollectionOverview.css';
|
||||
|
||||
import 'slick-carousel/slick/slick.css';
|
||||
import 'slick-carousel/slick/slick-theme.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
@@ -105,6 +109,7 @@ class CollectionOverview extends Component {
|
||||
const {
|
||||
showDetails,
|
||||
showOverview,
|
||||
showPosters,
|
||||
detailedProgressBar
|
||||
} = this.props.overviewOptions;
|
||||
|
||||
@@ -115,6 +120,15 @@ class CollectionOverview extends Component {
|
||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||
const overviewHeight = contentHeight - titleRowHeight - posterHeight;
|
||||
|
||||
const sliderSettings = {
|
||||
arrows: false,
|
||||
dots: false,
|
||||
infinite: false,
|
||||
slidesToShow: 1,
|
||||
slidesToScroll: 1,
|
||||
variableWidth: true
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
@@ -149,21 +163,25 @@ class CollectionOverview extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.navigationButtons}>
|
||||
<IconButton
|
||||
name={icons.ARROW_LEFT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickPrev}
|
||||
size={20}
|
||||
/>
|
||||
{
|
||||
showPosters &&
|
||||
<div className={styles.navigationButtons}>
|
||||
<IconButton
|
||||
name={icons.ARROW_LEFT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickPrev}
|
||||
size={20}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.ARROW_RIGHT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickNext}
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<IconButton
|
||||
name={icons.ARROW_RIGHT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickNext}
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -249,22 +267,35 @@ class CollectionOverview extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.sliderContainer}>
|
||||
<Carousel setRef={this.setSliderRef}>
|
||||
{movies.map((movie) => (
|
||||
<div className={styles.movie} key={movie.tmdbId}>
|
||||
<CollectionMovieConnector
|
||||
{
|
||||
showPosters ?
|
||||
<div className={styles.sliderContainer}>
|
||||
<Slider ref={this.setSliderRef} {...sliderSettings}>
|
||||
{movies.map((movie) => (
|
||||
<div className={styles.movie} key={movie.tmdbId}>
|
||||
<CollectionMovieConnector
|
||||
key={movie.tmdbId}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
collectionId={id}
|
||||
{...movie}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Slider>
|
||||
</div> :
|
||||
<div className={styles.labelsContainer}>
|
||||
{movies.map((movie) => (
|
||||
<CollectionMovieLabelConnector
|
||||
key={movie.tmdbId}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
collectionId={id}
|
||||
{...movie}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ function calculatePosterWidth(posterSize, isSmallScreen) {
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
overviewOptions.showPosters ? posterHeight : 75,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ class CollectionOverviewOptionsModalContent extends Component {
|
||||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showDetails: props.showDetails,
|
||||
showOverview: props.showOverview
|
||||
showOverview: props.showOverview,
|
||||
showPosters: props.showPosters
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +41,8 @@ class CollectionOverviewOptionsModalContent extends Component {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showDetails,
|
||||
showOverview
|
||||
showOverview,
|
||||
showPosters
|
||||
} = this.props;
|
||||
|
||||
const state = {};
|
||||
@@ -61,6 +63,10 @@ class CollectionOverviewOptionsModalContent extends Component {
|
||||
state.showOverview = showOverview;
|
||||
}
|
||||
|
||||
if (showPosters !== prevProps.showPosters) {
|
||||
state.showPosters = showPosters;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(state)) {
|
||||
this.setState(state);
|
||||
}
|
||||
@@ -99,13 +105,14 @@ class CollectionOverviewOptionsModalContent extends Component {
|
||||
size,
|
||||
detailedProgressBar,
|
||||
showDetails,
|
||||
showPosters,
|
||||
showOverview
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Overview Options
|
||||
{translate('CollectionOptions')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -141,6 +148,7 @@ class CollectionOverviewOptionsModalContent extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="showDetails"
|
||||
value={showDetails}
|
||||
helpText={translate('CollectionShowDetailsHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -152,6 +160,19 @@ class CollectionOverviewOptionsModalContent extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="showOverview"
|
||||
value={showOverview}
|
||||
helpText={translate('CollectionShowOverviewsHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowPosters')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showPosters"
|
||||
value={showPosters}
|
||||
helpText={translate('CollectionShowPostersHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -175,6 +196,7 @@ CollectionOverviewOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showDetails: PropTypes.bool.isRequired,
|
||||
showOverview: PropTypes.bool.isRequired,
|
||||
showPosters: PropTypes.bool.isRequired,
|
||||
onChangeOverviewOption: PropTypes.func.isRequired,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Slider from 'react-slick';
|
||||
import styles from './Alert.css';
|
||||
|
||||
import 'slick-carousel/slick/slick.css';
|
||||
import 'slick-carousel/slick/slick-theme.css';
|
||||
|
||||
function Carousel({ className, setRef, children, ...otherProps }) {
|
||||
|
||||
const sliderSettings = {
|
||||
arrows: false,
|
||||
dots: false,
|
||||
infinite: false,
|
||||
slidesToShow: 1,
|
||||
slidesToScroll: 1,
|
||||
variableWidth: true
|
||||
};
|
||||
|
||||
return (
|
||||
<Slider ref={setRef} {...sliderSettings}>
|
||||
{children}
|
||||
</Slider>
|
||||
);
|
||||
}
|
||||
|
||||
Carousel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
setRef: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
Carousel.defaultProps = {
|
||||
className: styles.alert
|
||||
};
|
||||
|
||||
export default Carousel;
|
||||
@@ -1,5 +0,0 @@
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -22,11 +22,11 @@ class ImdbRating extends PureComponent {
|
||||
let ratingString = '0.0';
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value}`;
|
||||
ratingString = `${rating.value.toFixed(1)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span title={`${rating.votes} votes`}>
|
||||
<span title={`${rating ? rating.votes : 0} votes`}>
|
||||
{
|
||||
!hideIcon &&
|
||||
<img
|
||||
|
||||
@@ -14,7 +14,7 @@ function PageContent(props) {
|
||||
|
||||
return (
|
||||
<ErrorBoundary errorComponent={PageContentError}>
|
||||
<DocumentTitle title={title ? `${title} - Radarr` : 'Radarr'}>
|
||||
<DocumentTitle title={title ? `${title} - ${window.Radarr.instanceName}` : window.Radarr.instanceName}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -21,9 +21,11 @@ class RottenTomatoRating extends PureComponent {
|
||||
const rating = ratings.rottenTomatoes;
|
||||
|
||||
let ratingString = '0%';
|
||||
let ratingImage = rtFresh;
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value}%`;
|
||||
ratingImage = rating.value > 50 ? rtFresh : rtRotten;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,7 +34,7 @@ class RottenTomatoRating extends PureComponent {
|
||||
!hideIcon &&
|
||||
<img
|
||||
className={styles.image}
|
||||
src={rating.value > 50 ? rtFresh : rtRotten}
|
||||
src={ratingImage}
|
||||
style={{
|
||||
height: `${iconSize}px`
|
||||
}}
|
||||
|
||||
@@ -22,7 +22,7 @@ class TmdbRating extends PureComponent {
|
||||
let ratingString = '0%';
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value * 10}%`;
|
||||
ratingString = `${(rating.value * 10).toFixed()}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DiscoverMovieFooterLabel from './DiscoverMovieFooterLabel';
|
||||
import ExcludeMovieModal from './Exclusion/ExcludeMovieModal';
|
||||
@@ -137,11 +138,6 @@ class DiscoverMovieFooter extends Component {
|
||||
isExcludeMovieModalOpen
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: true, value: translate('Monitored') },
|
||||
{ key: false, value: translate('Unmonitored') }
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
@@ -153,7 +149,7 @@ class DiscoverMovieFooter extends Component {
|
||||
<SelectInput
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
values={monitoredOptions}
|
||||
values={monitorOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
@@ -111,7 +111,7 @@ function DiscoverMoviePosterInfo(props) {
|
||||
if (sortKey === 'ratings' && ratings) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImportListListConnector from 'Components/ImportListListConnector';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
@@ -8,6 +7,7 @@ import Link from 'Components/Link/Link';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
@@ -245,7 +245,7 @@ class DiscoverMovieRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -81,6 +81,7 @@ const filterExistingFilesOptions = {
|
||||
};
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'chooseImportMode', value: translate('ChooseImportMode'), disabled: true },
|
||||
{ key: 'move', value: translate('MoveFiles') },
|
||||
{ key: 'copy', value: translate('HardlinkCopyFiles') }
|
||||
];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -101,9 +100,18 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
};
|
||||
|
||||
onImportSelectedPress = (selected, importMode) => {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const files = [];
|
||||
|
||||
_.forEach(this.props.items, (item) => {
|
||||
if (importMode === 'chooseImportMode') {
|
||||
this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' });
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const isSelected = selected.indexOf(item.id) > -1;
|
||||
|
||||
if (isSelected) {
|
||||
|
||||
@@ -7,3 +7,9 @@
|
||||
.filteredMessage {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.blankpad {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
@@ -2,15 +2,12 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
||||
import styles from './InteractiveSearch.css';
|
||||
import styles from './InteractiveSearchContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -25,6 +22,20 @@ const columns = [
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, { name: icons.DANGER }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: translate('Title'),
|
||||
@@ -88,24 +99,10 @@ const columns = [
|
||||
label: React.createElement(Icon, { name: icons.FLAG }),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, { name: icons.DANGER }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function InteractiveSearch(props) {
|
||||
function InteractiveSearchContent(props) {
|
||||
const {
|
||||
searchPayload,
|
||||
isFetching,
|
||||
@@ -113,63 +110,44 @@ function InteractiveSearch(props) {
|
||||
error,
|
||||
totalReleasesCount,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onSortPress,
|
||||
onFilterSelect,
|
||||
onGrabPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||
filterModalConnectorComponentProps={'movies'}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<div>
|
||||
!isFetching && !!error &&
|
||||
<div className={styles.blankpad}>
|
||||
{translate('UnableToLoadResultsIntSearch')}
|
||||
</div> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated && !totalReleasesCount ?
|
||||
<div>
|
||||
!isFetching && isPopulated && !totalReleasesCount &&
|
||||
<div className={styles.blankpad}>
|
||||
{translate('NoResultsFound')}
|
||||
</div> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!totalReleasesCount && isPopulated && !items.length ?
|
||||
<div>
|
||||
!!totalReleasesCount && isPopulated && !items.length &&
|
||||
<div className={styles.blankpad}>
|
||||
{translate('AllResultsHiddenFilter')}
|
||||
</div> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length ?
|
||||
isPopulated && !!items.length &&
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
@@ -181,7 +159,7 @@ function InteractiveSearch(props) {
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRowConnector
|
||||
key={item.guid}
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
longDateFormat={longDateFormat}
|
||||
@@ -192,38 +170,32 @@ function InteractiveSearch(props) {
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
</Table>
|
||||
}
|
||||
|
||||
{
|
||||
totalReleasesCount !== items.length && !!items.length ?
|
||||
totalReleasesCount !== items.length && !!items.length &&
|
||||
<div className={styles.filteredMessage}>
|
||||
{translate('SomeResultsHiddenFilter')}
|
||||
</div> :
|
||||
null
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearch.propTypes = {
|
||||
InteractiveSearchContent.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalReleasesCount: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveSearch;
|
||||
export default InteractiveSearchContent;
|
||||
@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
|
||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import InteractiveSearch from './InteractiveSearch';
|
||||
import InteractiveSearchContent from './InteractiveSearchContent';
|
||||
|
||||
function createMapStateToProps(appState) {
|
||||
return createSelector(
|
||||
@@ -48,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
};
|
||||
}
|
||||
|
||||
class InteractiveSearchConnector extends Component {
|
||||
class InteractiveSearchContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -79,18 +79,18 @@ class InteractiveSearchConnector extends Component {
|
||||
|
||||
return (
|
||||
|
||||
<InteractiveSearch
|
||||
<InteractiveSearchContent
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchConnector.propTypes = {
|
||||
InteractiveSearchContentConnector.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchClearReleases: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
|
||||
@@ -1,20 +1,15 @@
|
||||
.protocol {
|
||||
.cell {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
}
|
||||
|
||||
.protocol {
|
||||
composes: cell;
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.title {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell;
|
||||
|
||||
width: 85px;
|
||||
}
|
||||
@@ -22,9 +17,7 @@
|
||||
.quality,
|
||||
.customFormat,
|
||||
.language {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
text-align: center;
|
||||
composes: cell;
|
||||
}
|
||||
|
||||
.language {
|
||||
@@ -32,7 +25,7 @@
|
||||
}
|
||||
|
||||
.customFormatScore {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell;
|
||||
|
||||
width: 55px;
|
||||
font-weight: bold;
|
||||
@@ -42,26 +35,34 @@
|
||||
.rejected,
|
||||
.indexerFlags,
|
||||
.download {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell;
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.age,
|
||||
.size {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.peers {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell;
|
||||
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.title {
|
||||
composes: cell;
|
||||
}
|
||||
|
||||
.title div {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.history {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell;
|
||||
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,46 @@ class InteractiveSearchRow extends Component {
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.download}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{
|
||||
!!rejections.length &&
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
}
|
||||
title={translate('ReleaseRejected')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
rejections.map((rejection, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{rejection}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.title}>
|
||||
<Link
|
||||
to={infoUrl}
|
||||
@@ -257,46 +297,6 @@ class InteractiveSearchRow extends Component {
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{
|
||||
!!rejections.length &&
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
}
|
||||
title={translate('ReleaseRejected')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
rejections.map((rejection, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{rejection}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.download}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isConfirmGrabModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
|
||||
16
frontend/src/InteractiveSearch/InteractiveSearchTable.js
Normal file
16
frontend/src/InteractiveSearch/InteractiveSearchTable.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
|
||||
|
||||
function InteractiveSearchTable(props) {
|
||||
|
||||
return (
|
||||
<InteractiveSearchContentConnector
|
||||
searchPayload={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearchTable.propTypes = {
|
||||
};
|
||||
|
||||
export default InteractiveSearchTable;
|
||||
@@ -69,8 +69,7 @@ class MovieCastPoster extends Component {
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
borderRadius: '5px'
|
||||
height: `${posterHeight}px`
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
|
||||
@@ -69,8 +69,7 @@ class MovieCrewPoster extends Component {
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
borderRadius: '5px'
|
||||
height: `${posterHeight}px`
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.content {
|
||||
border-radius: 5px;
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.movie {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Carousel from 'Components/Carousel';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import Measure from 'Components/Measure';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
||||
@@ -168,36 +169,56 @@ class MovieCreditPosters extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
itemComponent
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
posterWidth,
|
||||
posterHeight
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
const rowCount = Math.ceil(items.length / columnCount);
|
||||
|
||||
<div className={styles.sliderContainer}>
|
||||
<Carousel setRef={this.setSliderRef}>
|
||||
{items.map((movie) => (
|
||||
<div className={styles.movie} key={movie.tmdbId}>
|
||||
<MovieCreditPosterConnector
|
||||
key={movie.order}
|
||||
component={itemComponent}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
tmdbId={movie.personTmdbId}
|
||||
personName={movie.personName}
|
||||
job={movie.job}
|
||||
character={movie.character}
|
||||
images={movie.images}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<WindowScroller
|
||||
scrollElement={undefined}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
ref={this.setGridRef}
|
||||
className={styles.grid}
|
||||
autoHeight={true}
|
||||
height={height}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
rowCount={rowCount}
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptOut={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
3
frontend/src/Movie/Details/MovieAlternateTitles.css
Normal file
3
frontend/src/Movie/Details/MovieAlternateTitles.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.alternateTitle {
|
||||
white-space: nowrap;
|
||||
}
|
||||
28
frontend/src/Movie/Details/MovieAlternateTitles.js
Normal file
28
frontend/src/Movie/Details/MovieAlternateTitles.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './MovieAlternateTitles.css';
|
||||
|
||||
function MovieAlternateTitles({ alternateTitles }) {
|
||||
return (
|
||||
<ul>
|
||||
{
|
||||
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
|
||||
return (
|
||||
<li
|
||||
key={alternateTitle}
|
||||
className={styles.alternateTitle}
|
||||
>
|
||||
{alternateTitle}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
MovieAlternateTitles.propTypes = {
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
};
|
||||
|
||||
export default MovieAlternateTitles;
|
||||
@@ -5,7 +5,7 @@
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 425px;
|
||||
height: 375px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
@@ -39,11 +39,10 @@
|
||||
}
|
||||
|
||||
.poster {
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
width: 250px;
|
||||
height: 368px;
|
||||
width: 217px;
|
||||
height: 319px;
|
||||
}
|
||||
|
||||
.info {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import InfoLabel from 'Components/InfoLabel';
|
||||
@@ -22,11 +22,12 @@ import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
@@ -80,10 +81,10 @@ class MovieDetails extends Component {
|
||||
isEditMovieModalOpen: false,
|
||||
isDeleteMovieModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isInteractiveSearchModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {},
|
||||
selectedTabIndex: 0,
|
||||
overviewHeight: 0,
|
||||
titleWidth: 0
|
||||
};
|
||||
@@ -136,14 +137,6 @@ class MovieDetails extends Component {
|
||||
this.setState({ isEditMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onInteractiveSearchPress = () => {
|
||||
this.setState({ isInteractiveSearchModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveSearchModalClose = () => {
|
||||
this.setState({ isInteractiveSearchModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteMoviePress = () => {
|
||||
this.setState({
|
||||
isEditMovieModalOpen: false,
|
||||
@@ -305,9 +298,9 @@ class MovieDetails extends Component {
|
||||
isEditMovieModalOpen,
|
||||
isDeleteMovieModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isInteractiveSearchModalOpen,
|
||||
overviewHeight,
|
||||
titleWidth
|
||||
titleWidth,
|
||||
selectedTabIndex
|
||||
} = this.state;
|
||||
|
||||
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
||||
@@ -333,14 +326,6 @@ class MovieDetails extends Component {
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('InteractiveSearch')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
isSpinning={isSearching}
|
||||
title={undefined}
|
||||
onPress={this.onInteractiveSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
@@ -666,39 +651,101 @@ class MovieDetails extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
<FieldSet legend={translate('History')}>
|
||||
<MovieHistoryTable
|
||||
movieId={id}
|
||||
/>
|
||||
</FieldSet>
|
||||
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
|
||||
<TabList
|
||||
className={styles.tabList}
|
||||
>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('History')}
|
||||
</Tab>
|
||||
|
||||
<FieldSet legend={translate('Files')}>
|
||||
<MovieFileEditorTable
|
||||
movieId={id}
|
||||
/>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Search')}
|
||||
</Tab>
|
||||
|
||||
<ExtraFileTable
|
||||
movieId={id}
|
||||
/>
|
||||
</FieldSet>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Files')}
|
||||
</Tab>
|
||||
|
||||
<FieldSet legend={translate('Cast')}>
|
||||
<MovieCastPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Titles')}
|
||||
</Tab>
|
||||
|
||||
<FieldSet legend={translate('Crew')}>
|
||||
<MovieCrewPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Cast')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Crew')}
|
||||
</Tab>
|
||||
|
||||
{
|
||||
selectedTabIndex === 1 &&
|
||||
<div className={styles.filterIcon}>
|
||||
<InteractiveSearchFilterMenuConnector />
|
||||
</div>
|
||||
}
|
||||
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<MovieHistoryTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<InteractiveSearchTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieFileEditorTable
|
||||
movieId={id}
|
||||
/>
|
||||
<ExtraFileTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieTitlesTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieCastPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieCrewPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<FieldSet legend={translate('Titles')}>
|
||||
<MovieTitlesTable
|
||||
movieId={id}
|
||||
/>
|
||||
</FieldSet>
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
@@ -730,12 +777,6 @@ class MovieDetails extends Component {
|
||||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<MovieInteractiveSearchModalConnector
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onInteractiveSearchModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.container {
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
||||
import styles from './MovieTitlesTable.css';
|
||||
|
||||
function MovieTitlesTable(props) {
|
||||
const {
|
||||
@@ -8,11 +7,9 @@ function MovieTitlesTable(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<MovieTitlesTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
<MovieTitlesTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.container {
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
|
||||
import styles from './MovieHistoryTable.css';
|
||||
|
||||
function MovieHistoryTable(props) {
|
||||
const {
|
||||
@@ -8,11 +7,9 @@ function MovieHistoryTable(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<MovieHistoryTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
<MovieHistoryTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,12 +102,21 @@ function MovieIndexSortMenu(props) {
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="ratings"
|
||||
name="imdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Ratings')}
|
||||
{translate('ImdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="tmdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('TmdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
||||
@@ -77,7 +77,9 @@
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
.ratings {
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -84,7 +84,9 @@
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
.ratings {
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
@@ -349,13 +351,39 @@ class MovieIndexRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
if (name === 'tmdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'rottenTomatoesRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<RottenTomatoRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'imdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<ImdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import MovieInteractiveSearchModalContent from './MovieInteractiveSearchModalContent';
|
||||
|
||||
function MovieInteractiveSearchModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
movieId,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
>
|
||||
<MovieInteractiveSearchModalContent
|
||||
movieId={movieId}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
MovieInteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
movieId: PropTypes.number.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieInteractiveSearchModal;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
import MovieInteractiveSearchModal from './MovieInteractiveSearchModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onModalClose() {
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
props.onModalClose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(MovieInteractiveSearchModal);
|
||||
@@ -1,45 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
|
||||
function MovieInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
movieId,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Interactive Search
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
<InteractiveSearchConnector
|
||||
searchPayload={{
|
||||
movieId
|
||||
}}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
MovieInteractiveSearchModalContent.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieInteractiveSearchModalContent;
|
||||
@@ -1,4 +1,5 @@
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
|
||||
@@ -65,10 +65,10 @@ function DownloadClientOptions(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="checkForFinishedDownloadInterval"
|
||||
min={0}
|
||||
min={1}
|
||||
max={120}
|
||||
unit="minutes"
|
||||
helpText={translate('HelpText')}
|
||||
helpText={translate('RefreshMonitoredIntervalHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.checkForFinishedDownloadInterval}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,7 @@ function HostSettings(props) {
|
||||
bindAddress,
|
||||
port,
|
||||
urlBase,
|
||||
instanceName,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertPath,
|
||||
@@ -73,6 +74,22 @@ function HostSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('InstanceName')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="instanceName"
|
||||
helpText={translate('InstanceNameHelpText')}
|
||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...instanceName}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -116,7 +116,7 @@ function IndexerOptions(props) {
|
||||
min={0}
|
||||
max={120}
|
||||
unit="minutes"
|
||||
helpText={translate('HelpText')}
|
||||
helpText={translate('RssSyncHelpText')}
|
||||
helpTextWarning={translate('RSSSyncIntervalHelpTextWarning')}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#how-does-radarr-work"
|
||||
onChange={onInputChange}
|
||||
|
||||
@@ -59,6 +59,7 @@ class Notification extends Component {
|
||||
onDownload,
|
||||
onUpgrade,
|
||||
onRename,
|
||||
onMovieAdded,
|
||||
onMovieDelete,
|
||||
onMovieFileDelete,
|
||||
onMovieFileDeleteForUpgrade,
|
||||
@@ -68,6 +69,7 @@ class Notification extends Component {
|
||||
supportsOnDownload,
|
||||
supportsOnUpgrade,
|
||||
supportsOnRename,
|
||||
supportsOnMovieAdded,
|
||||
supportsOnMovieDelete,
|
||||
supportsOnMovieFileDelete,
|
||||
supportsOnMovieFileDeleteForUpgrade,
|
||||
@@ -117,6 +119,14 @@ class Notification extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnMovieAdded && onMovieAdded ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnMovieAdded')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnHealthIssue && onHealthIssue ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
@@ -196,6 +206,7 @@ Notification.propTypes = {
|
||||
onDownload: PropTypes.bool.isRequired,
|
||||
onUpgrade: PropTypes.bool.isRequired,
|
||||
onRename: PropTypes.bool.isRequired,
|
||||
onMovieAdded: PropTypes.bool.isRequired,
|
||||
onMovieDelete: PropTypes.bool.isRequired,
|
||||
onMovieFileDelete: PropTypes.bool.isRequired,
|
||||
onMovieFileDeleteForUpgrade: PropTypes.bool.isRequired,
|
||||
@@ -208,6 +219,7 @@ Notification.propTypes = {
|
||||
supportsOnMovieFileDeleteForUpgrade: PropTypes.bool.isRequired,
|
||||
supportsOnUpgrade: PropTypes.bool.isRequired,
|
||||
supportsOnRename: PropTypes.bool.isRequired,
|
||||
supportsOnMovieAdded: PropTypes.bool.isRequired,
|
||||
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
||||
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteNotification: PropTypes.func.isRequired
|
||||
|
||||
@@ -19,6 +19,7 @@ function NotificationEventItems(props) {
|
||||
onDownload,
|
||||
onUpgrade,
|
||||
onRename,
|
||||
onMovieAdded,
|
||||
onMovieDelete,
|
||||
onMovieFileDelete,
|
||||
onMovieFileDeleteForUpgrade,
|
||||
@@ -28,6 +29,7 @@ function NotificationEventItems(props) {
|
||||
supportsOnDownload,
|
||||
supportsOnUpgrade,
|
||||
supportsOnRename,
|
||||
supportsOnMovieAdded,
|
||||
supportsOnMovieDelete,
|
||||
supportsOnMovieFileDelete,
|
||||
supportsOnMovieFileDeleteForUpgrade,
|
||||
@@ -92,6 +94,17 @@ function NotificationEventItems(props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onMovieAdded"
|
||||
helpText={translate('OnMovieAddedHelpText')}
|
||||
isDisabled={!supportsOnMovieAdded.value}
|
||||
{...onMovieAdded}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
|
||||
@@ -106,6 +106,7 @@ export default {
|
||||
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
|
||||
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
||||
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
||||
selectedSchema.onMovieAdded = selectedSchema.supportsOnMovieAdded;
|
||||
selectedSchema.onMovieDelete = selectedSchema.supportsOnMovieDelete;
|
||||
selectedSchema.onMovieFileDelete = selectedSchema.supportsOnMovieFileDelete;
|
||||
selectedSchema.onMovieFileDeleteForUpgrade = selectedSchema.supportsOnMovieFileDeleteForUpgrade;
|
||||
|
||||
@@ -561,7 +561,7 @@ export const actionHandlers = handleThunks({
|
||||
}, []);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/movie/import',
|
||||
url: '/importlist/movie',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(allNewMovies)
|
||||
|
||||
@@ -79,6 +79,11 @@ export const defaultState = {
|
||||
label: translate('ReleaseGroup'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: translate('SourceTitle'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
columnLabel: translate('Details'),
|
||||
|
||||
@@ -30,7 +30,7 @@ export const defaultState = {
|
||||
sortKey: 'quality',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
recentFolders: [],
|
||||
importMode: 'move',
|
||||
importMode: 'chooseImportMode',
|
||||
sortPredicates: {
|
||||
relativePath: function(item, direction) {
|
||||
const relativePath = item.relativePath;
|
||||
|
||||
@@ -162,6 +162,14 @@ export const filterPredicates = {
|
||||
return predicate(rating, filterValue);
|
||||
},
|
||||
|
||||
rottenTomatoesRating: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
const rating = item.ratings.rottenTomatoes ? item.ratings.rottenTomatoes.value : 0;
|
||||
|
||||
return predicate(rating, filterValue);
|
||||
},
|
||||
|
||||
imdbVotes: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getNewMovie from 'Utilities/Movie/getNewMovie';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { set, update, updateItem } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
@@ -40,7 +42,8 @@ export const defaultState = {
|
||||
detailedProgressBar: false,
|
||||
size: 'medium',
|
||||
showDetails: true,
|
||||
showOverview: true
|
||||
showOverview: true,
|
||||
showPosters: true
|
||||
},
|
||||
|
||||
defaults: {
|
||||
@@ -62,19 +65,81 @@ export const defaultState = {
|
||||
}
|
||||
],
|
||||
|
||||
filterPredicates: {},
|
||||
filterPredicates: {
|
||||
genres: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
let allGenres = [];
|
||||
item.movies.forEach((movie) => {
|
||||
allGenres = allGenres.concat(movie.genres);
|
||||
});
|
||||
|
||||
const genres = Array.from(new Set(allGenres)).slice(0, 3);
|
||||
|
||||
return predicate(genres, filterValue);
|
||||
},
|
||||
totalMovies: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const { movies } = item;
|
||||
|
||||
const totalMovies = movies.length;
|
||||
return predicate(totalMovies, filterValue);
|
||||
}
|
||||
},
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
label: translate('Title'),
|
||||
type: filterBuilderTypes.STRING
|
||||
},
|
||||
{
|
||||
name: 'monitored',
|
||||
label: 'Monitored',
|
||||
label: translate('Monitored'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: translate('QualityProfile'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.QUALITY_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'rootFolderPath',
|
||||
label: translate('RootFolder'),
|
||||
type: filterBuilderTypes.STRING
|
||||
},
|
||||
{
|
||||
name: 'genres',
|
||||
label: translate('Genres'),
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
optionsSelector: function(items) {
|
||||
const genreList = items.reduce((acc, collection) => {
|
||||
let collectionGenres = [];
|
||||
collection.movies.forEach((movie) => {
|
||||
collectionGenres = collectionGenres.concat(movie.genres);
|
||||
});
|
||||
|
||||
const genres = Array.from(new Set(collectionGenres)).slice(0, 3);
|
||||
|
||||
genres.forEach((genre) => {
|
||||
acc.push({
|
||||
id: genre,
|
||||
name: genre
|
||||
});
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return genreList.sort(sortByName);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'totalMovies',
|
||||
label: translate('TotalMovies'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -253,27 +318,32 @@ export const actionHandlers = handleThunks({
|
||||
const {
|
||||
collectionIds,
|
||||
monitored,
|
||||
monitor
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
rootFolderPath,
|
||||
minimumAvailability
|
||||
} = payload;
|
||||
|
||||
const response = {};
|
||||
const collections = [];
|
||||
|
||||
collectionIds.forEach((id) => {
|
||||
const collectionToUpdate = { id };
|
||||
|
||||
if (payload.hasOwnProperty('monitored')) {
|
||||
collectionToUpdate.monitored = monitored;
|
||||
}
|
||||
|
||||
collections.push(collectionToUpdate);
|
||||
});
|
||||
if (payload.hasOwnProperty('monitored')) {
|
||||
response.monitored = monitored;
|
||||
}
|
||||
|
||||
if (payload.hasOwnProperty('monitor')) {
|
||||
response.monitorMovies = monitor === 'monitored';
|
||||
}
|
||||
|
||||
response.collections = collections;
|
||||
if (payload.hasOwnProperty('qualityProfileId')) {
|
||||
response.qualityProfileId = qualityProfileId;
|
||||
}
|
||||
|
||||
if (payload.hasOwnProperty('minimumAvailability')) {
|
||||
response.minimumAvailability = minimumAvailability;
|
||||
}
|
||||
|
||||
response.rootFolderPath = rootFolderPath;
|
||||
response.collectionIds = collectionIds;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
|
||||
@@ -165,11 +165,11 @@ export const actionHandlers = handleThunks({
|
||||
requestData.quality = quality;
|
||||
}
|
||||
|
||||
if (releaseGroup) {
|
||||
if (releaseGroup !== undefined) {
|
||||
requestData.releaseGroup = releaseGroup;
|
||||
}
|
||||
|
||||
if (edition) {
|
||||
if (edition !== undefined) {
|
||||
requestData.edition = edition;
|
||||
}
|
||||
|
||||
@@ -201,11 +201,11 @@ export const actionHandlers = handleThunks({
|
||||
props.quality = quality;
|
||||
}
|
||||
|
||||
if (edition) {
|
||||
if (edition !== undefined) {
|
||||
props.edition = edition;
|
||||
}
|
||||
|
||||
if (releaseGroup) {
|
||||
if (releaseGroup !== undefined) {
|
||||
props.releaseGroup = releaseGroup;
|
||||
}
|
||||
|
||||
|
||||
@@ -178,8 +178,20 @@ export const defaultState = {
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'ratings',
|
||||
label: translate('Ratings'),
|
||||
name: 'tmdbRating',
|
||||
label: translate('TmdbRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'rottenTomatoesRating',
|
||||
label: translate('RottenTomatoesRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'imdbRating',
|
||||
label: translate('ImdbRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
@@ -224,10 +236,22 @@ export const defaultState = {
|
||||
return originalLanguage.name;
|
||||
},
|
||||
|
||||
ratings: function(item) {
|
||||
imdbRating: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
return ratings.tmdb? ratings.tmdb.value : 0;
|
||||
return ratings.imdb ? ratings.imdb.value : 0;
|
||||
},
|
||||
|
||||
tmdbRating: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
return ratings.tmdb ? ratings.tmdb.value : 0;
|
||||
},
|
||||
|
||||
rottenTomatoesRating: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
return ratings.rottenTomatoes ? ratings.rottenTomatoes.value : 0;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -413,6 +437,11 @@ export const defaultState = {
|
||||
label: translate('ImdbRating'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'rottenTomatoesRating',
|
||||
label: translate('RottenTomatoesRating'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'imdbVotes',
|
||||
label: translate('ImdbVotes'),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import migrateBlacklistToBlocklist from './migrateBlacklistToBlocklist';
|
||||
import migrateMonitorToEnum from './migrateMonitorToEnum';
|
||||
import migratePreDbToReleased from './migratePreDbToReleased';
|
||||
|
||||
export default function migrate(persistedState) {
|
||||
migrateBlacklistToBlocklist(persistedState);
|
||||
migratePreDbToReleased(persistedState);
|
||||
migrateMonitorToEnum(persistedState);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import get from 'lodash';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function migrateMonitorToEnum(persistedState) {
|
||||
const addMovie = get(persistedState, 'addMovie.defaults.monitor');
|
||||
const discoverMovie = get(persistedState, 'discoverMovie.defaults.monitor');
|
||||
const addMovie = _.get(persistedState, 'addMovie.defaults.monitor');
|
||||
const discoverMovie = _.get(persistedState, 'discoverMovie.defaults.monitor');
|
||||
|
||||
if (!addMovie && !discoverMovie) {
|
||||
return;
|
||||
if (addMovie) {
|
||||
if (addMovie === 'true') {
|
||||
persistedState.addMovie.defaults.monitor = 'movieOnly';
|
||||
}
|
||||
|
||||
if (addMovie === 'false') {
|
||||
persistedState.addMovie.defaults.monitor = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (addMovie === true) {
|
||||
persistedState.addMovie.defaults.monitor = 'movieOnly';
|
||||
}
|
||||
if (discoverMovie) {
|
||||
if (discoverMovie === 'true') {
|
||||
persistedState.discoverMovie.defaults.monitor = 'movieOnly';
|
||||
}
|
||||
|
||||
if (discoverMovie === true) {
|
||||
persistedState.discoverMovie.defaults.monitor = 'movieOnly';
|
||||
}
|
||||
|
||||
if (addMovie === false) {
|
||||
persistedState.addMovie.defaults.minimumAvailability = 'none';
|
||||
}
|
||||
|
||||
if (discoverMovie === false) {
|
||||
persistedState.discoverMovie.defaults.minimumAvailability = 'none';
|
||||
if (discoverMovie === 'false') {
|
||||
persistedState.discoverMovie.defaults.monitor = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import get from 'lodash';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function migratePreDbToReleased(persistedState) {
|
||||
const addMovie = get(persistedState, 'addMovie.defaults.minimumAvailability');
|
||||
const discoverMovie = get(persistedState, 'discoverMovie.defaults.minimumAvailability');
|
||||
const addMovie = _.get(persistedState, 'addMovie.defaults.minimumAvailability');
|
||||
const discoverMovie = _.get(persistedState, 'discoverMovie.defaults.minimumAvailability');
|
||||
|
||||
if (!addMovie && !discoverMovie) {
|
||||
return;
|
||||
|
||||
@@ -44,7 +44,14 @@ function filter(items, state) {
|
||||
const predicate = filterPredicates[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
accepted = value.some((v) => predicate(item, v, type));
|
||||
if (
|
||||
type === filterTypes.NOT_CONTAINS ||
|
||||
type === filterTypes.NOT_EQUAL
|
||||
) {
|
||||
accepted = value.every((v) => predicate(item, v, type));
|
||||
} else {
|
||||
accepted = value.some((v) => predicate(item, v, type));
|
||||
}
|
||||
} else {
|
||||
accepted = predicate(item, value, type);
|
||||
}
|
||||
|
||||
22
package.json
22
package.json
@@ -30,7 +30,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"@microsoft/signalr": "6.0.3",
|
||||
"@microsoft/signalr": "6.0.8",
|
||||
"@sentry/browser": "6.18.2",
|
||||
"@sentry/integrations": "6.18.2",
|
||||
"classnames": "2.3.1",
|
||||
@@ -45,7 +45,7 @@
|
||||
"jquery": "3.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.1",
|
||||
"moment": "2.29.2",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.7.2",
|
||||
@@ -101,8 +101,8 @@
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.11.0",
|
||||
"css-loader": "5.2.4",
|
||||
"core-js": "3.12.1",
|
||||
"css-loader": "6.5.1",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
@@ -117,22 +117,22 @@
|
||||
"mini-css-extract-plugin": "1.5.0",
|
||||
"postcss": "8.2.12",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "5.2.0",
|
||||
"postcss-mixins": "7.0.3",
|
||||
"postcss-nested": "5.0.5",
|
||||
"postcss-loader": "6.2.0",
|
||||
"postcss-mixins": "8.1.0",
|
||||
"postcss-nested": "5.0.6",
|
||||
"postcss-simple-vars": "6.0.3",
|
||||
"postcss-url": "10.1.3",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "2.0.0",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.6.0",
|
||||
"stylelint-order": "5.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.35.1",
|
||||
"webpack-cli": "4.6.0",
|
||||
"webpack-livereload-plugin": "3.0.1",
|
||||
"webpack": "5.64.2",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack-livereload-plugin": "3.0.2",
|
||||
"worker-loader": "3.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
||||
<add key="FFMpegCore" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FFMpegCore/nuget/v3/index.json" />
|
||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
||||
@@ -20,7 +20,21 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
|
||||
private static Exception[] FilteredExceptions = new Exception[]
|
||||
{
|
||||
new UnauthorizedAccessException()
|
||||
new UnauthorizedAccessException(),
|
||||
new AggregateException(new Exception[]
|
||||
{
|
||||
new UnauthorizedAccessException(),
|
||||
new UnauthorizedAccessException()
|
||||
})
|
||||
};
|
||||
|
||||
private static Exception[] NonFilteredExceptions = new Exception[]
|
||||
{
|
||||
new AggregateException(new Exception[]
|
||||
{
|
||||
new UnauthorizedAccessException(),
|
||||
new NotImplementedException()
|
||||
})
|
||||
};
|
||||
|
||||
[SetUp]
|
||||
@@ -63,6 +77,14 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
_subject.IsSentryMessage(log).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource("NonFilteredExceptions")]
|
||||
public void should_not_filter_event_for_filtered_exception_types(Exception ex)
|
||||
{
|
||||
var log = GivenLogEvent(LogLevel.Error, ex, "test");
|
||||
_subject.IsSentryMessage(log).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource("FilteredExceptions")]
|
||||
public void should_not_filter_event_for_filtered_exception_types_if_filtering_disabled(Exception ex)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace NzbDrone.Common.EnvironmentInfo
|
||||
{
|
||||
@@ -51,78 +50,5 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
{
|
||||
return _version;
|
||||
}
|
||||
|
||||
private static Version GetDotNetVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\";
|
||||
using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey))
|
||||
{
|
||||
if (ndpKey == null)
|
||||
{
|
||||
return new Version(4, 0);
|
||||
}
|
||||
|
||||
var releaseKey = (int)ndpKey.GetValue("Release");
|
||||
|
||||
if (releaseKey >= 528040)
|
||||
{
|
||||
return new Version(4, 8, 0);
|
||||
}
|
||||
|
||||
if (releaseKey >= 461808)
|
||||
{
|
||||
return new Version(4, 7, 2);
|
||||
}
|
||||
|
||||
if (releaseKey >= 461308)
|
||||
{
|
||||
return new Version(4, 7, 1);
|
||||
}
|
||||
|
||||
if (releaseKey >= 460798)
|
||||
{
|
||||
return new Version(4, 7);
|
||||
}
|
||||
|
||||
if (releaseKey >= 394802)
|
||||
{
|
||||
return new Version(4, 6, 2);
|
||||
}
|
||||
|
||||
if (releaseKey >= 394254)
|
||||
{
|
||||
return new Version(4, 6, 1);
|
||||
}
|
||||
|
||||
if (releaseKey >= 393295)
|
||||
{
|
||||
return new Version(4, 6);
|
||||
}
|
||||
|
||||
if (releaseKey >= 379893)
|
||||
{
|
||||
return new Version(4, 5, 2);
|
||||
}
|
||||
|
||||
if (releaseKey >= 378675)
|
||||
{
|
||||
return new Version(4, 5, 1);
|
||||
}
|
||||
|
||||
if (releaseKey >= 378389)
|
||||
{
|
||||
return new Version(4, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Couldnt get .NET framework version: " + ex.ToString());
|
||||
}
|
||||
|
||||
return new Version(4, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
|
||||
@@ -8,47 +10,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
||||
|
||||
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return logBuilder.Property("Sentry", fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
|
||||
}
|
||||
|
||||
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
{
|
||||
SentryLogger.Log(level)
|
||||
.CopyLogEvent(logBuilder.LogEventInfo)
|
||||
SentryLogger.ForLogEvent(level)
|
||||
.CopyLogEvent(logBuilder.LogEvent)
|
||||
.SentryFingerprint(fingerprint)
|
||||
.Write();
|
||||
.Log();
|
||||
|
||||
return logBuilder.Property("Sentry", null);
|
||||
return logBuilder.Property<string>("Sentry", null);
|
||||
}
|
||||
|
||||
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
|
||||
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
|
||||
{
|
||||
return logBuilder.LoggerName(logEvent.LoggerName)
|
||||
.TimeStamp(logEvent.TimeStamp)
|
||||
return logBuilder.TimeStamp(logEvent.TimeStamp)
|
||||
.Message(logEvent.Message, logEvent.Parameters)
|
||||
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
|
||||
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
|
||||
.Exception(logEvent.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class NzbDroneFileTarget : FileTarget
|
||||
{
|
||||
protected override string GetFormattedMessage(LogEventInfo logEvent)
|
||||
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||
{
|
||||
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
target.Append(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation
|
||||
|
||||
var appFolderInfo = new AppFolderInfo(startupContext);
|
||||
|
||||
RegisterGlobalFilters();
|
||||
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
RegisterDebugger();
|
||||
@@ -97,10 +99,21 @@ namespace NzbDrone.Common.Instrumentation
|
||||
target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
|
||||
|
||||
var loggingRule = new LoggingRule("*", LogLevel.Trace, target);
|
||||
|
||||
LogManager.Configuration.AddTarget("debugger", target);
|
||||
LogManager.Configuration.LoggingRules.Add(loggingRule);
|
||||
}
|
||||
|
||||
private static void RegisterGlobalFilters()
|
||||
{
|
||||
LogManager.Setup().LoadConfiguration(c =>
|
||||
{
|
||||
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
|
||||
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
|
||||
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
|
||||
});
|
||||
}
|
||||
|
||||
private static void RegisterConsole()
|
||||
{
|
||||
var level = LogLevel.Trace;
|
||||
|
||||
@@ -229,21 +229,48 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
if (FilterEvents)
|
||||
{
|
||||
var sqlEx = logEvent.Exception as SQLiteException;
|
||||
if (sqlEx != null && FilteredSQLiteErrors.Contains(sqlEx.ResultCode))
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
var aggEx = logEvent.Exception as AggregateException;
|
||||
|
||||
if (aggEx != null && aggEx.InnerExceptions.Count > 0)
|
||||
{
|
||||
return false;
|
||||
exceptions.AddRange(aggEx.InnerExceptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
exceptions.Add(logEvent.Exception);
|
||||
}
|
||||
|
||||
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
|
||||
// If any are sentry then send to sentry
|
||||
foreach (var ex in exceptions)
|
||||
{
|
||||
return false;
|
||||
var isSentry = true;
|
||||
|
||||
var sqlEx = ex as SQLiteException;
|
||||
if (sqlEx != null && !FilteredSQLiteErrors.Contains(sqlEx.ResultCode))
|
||||
{
|
||||
isSentry = false;
|
||||
}
|
||||
|
||||
if (FilteredExceptionTypeNames.Contains(ex.GetType().Name))
|
||||
{
|
||||
isSentry = false;
|
||||
}
|
||||
|
||||
if (FilteredExceptionMessages.Any(x => ex.Message.Contains(x)))
|
||||
{
|
||||
isSentry = false;
|
||||
}
|
||||
|
||||
if (isSentry)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (FilteredExceptionMessages.Any(x => logEvent.Exception.Message.Contains(x)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// The exception or aggregate exception children were not sentry exceptions
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -8,19 +8,18 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="Sentry" Version="3.15.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.0.1" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="Sentry" Version="3.20.1" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.5" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="EnsureThat\Resources\ExceptionMessages.Designer.cs">
|
||||
|
||||
@@ -27,6 +27,20 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
Mocker.Resolve<IDatabase>().Vacuum();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_should_not_contain_timestamp_without_timezone_columns()
|
||||
{
|
||||
if (Db.DatabaseType != DatabaseType.PostgreSQL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mocker.Resolve<IDatabase>()
|
||||
.OpenConnection().Query("SELECT table_name, column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'public' AND data_type = 'timestamp without time zone'")
|
||||
.Should()
|
||||
.BeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void get_version()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class movie_metadataFixture : MigrationTest<movie_metadata>
|
||||
{
|
||||
[Test]
|
||||
public void should_add_metadata_from_movie_and_link_back_to_movie()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Movies").Row(new
|
||||
{
|
||||
Monitored = true,
|
||||
Title = "Title",
|
||||
CleanTitle = "CleanTitle",
|
||||
Status = 3,
|
||||
MinimumAvailability = 4,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Recommendations = new[] { 1 }.ToJson(),
|
||||
Runtime = 90,
|
||||
OriginalLanguage = 1,
|
||||
ProfileId = 1,
|
||||
MovieFileId = 0,
|
||||
Path = string.Format("/Movies/{0}", "Title"),
|
||||
TitleSlug = 123456,
|
||||
TmdbId = 132456,
|
||||
Added = DateTime.UtcNow,
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
LastInfoSync = DateTime.UtcNow,
|
||||
});
|
||||
});
|
||||
|
||||
var metadata = db.Query<MovieMetadata207>("SELECT \"Id\", \"Title\", \"TmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
metadata.Should().HaveCount(1);
|
||||
metadata.First().TmdbId.Should().Be(132456);
|
||||
metadata.First().Title.Should().Be("Title");
|
||||
|
||||
var movies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"Movies\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_link_metadata_to_credits()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Movies").Row(new
|
||||
{
|
||||
Id = 5,
|
||||
Monitored = true,
|
||||
Title = "Title",
|
||||
CleanTitle = "CleanTitle",
|
||||
Status = 3,
|
||||
MinimumAvailability = 4,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Recommendations = new[] { 1 }.ToJson(),
|
||||
Runtime = 90,
|
||||
OriginalLanguage = 1,
|
||||
ProfileId = 1,
|
||||
MovieFileId = 0,
|
||||
Path = string.Format("/Movies/{0}", "Title"),
|
||||
TitleSlug = 123456,
|
||||
TmdbId = 132456,
|
||||
Added = DateTime.UtcNow,
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
LastInfoSync = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("Credits").Row(new
|
||||
{
|
||||
MovieId = 5,
|
||||
CreditTmdbId = 123,
|
||||
PersonTmdbId = 456,
|
||||
Order = 1,
|
||||
Type = 1,
|
||||
Name = "Some Person",
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson()
|
||||
});
|
||||
});
|
||||
|
||||
var metadata = db.Query<MovieMetadata207>("SELECT \"Id\", \"Title\", \"TmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
metadata.Should().HaveCount(1);
|
||||
metadata.First().TmdbId.Should().Be(132456);
|
||||
metadata.First().Title.Should().Be("Title");
|
||||
|
||||
var movies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"Credits\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_link_metadata_to_alt_title()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Movies").Row(new
|
||||
{
|
||||
Id = 5,
|
||||
Monitored = true,
|
||||
Title = "Title",
|
||||
CleanTitle = "CleanTitle",
|
||||
Status = 3,
|
||||
MinimumAvailability = 4,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Recommendations = new[] { 1 }.ToJson(),
|
||||
Runtime = 90,
|
||||
OriginalLanguage = 1,
|
||||
ProfileId = 1,
|
||||
MovieFileId = 0,
|
||||
Path = string.Format("/Movies/{0}", "Title"),
|
||||
TitleSlug = 123456,
|
||||
TmdbId = 132456,
|
||||
Added = DateTime.UtcNow,
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
LastInfoSync = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("AlternativeTitles").Row(new
|
||||
{
|
||||
MovieId = 5,
|
||||
Title = "Some Alt",
|
||||
CleanTitle = "somealt",
|
||||
SourceType = 1,
|
||||
SourceId = 1,
|
||||
Votes = 0,
|
||||
VoteCount = 0,
|
||||
Language = 1
|
||||
});
|
||||
});
|
||||
|
||||
var metadata = db.Query<MovieMetadata207>("SELECT \"Id\", \"Title\", \"TmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
metadata.Should().HaveCount(1);
|
||||
metadata.First().TmdbId.Should().Be(132456);
|
||||
metadata.First().Title.Should().Be("Title");
|
||||
|
||||
var movies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"AlternativeTitles\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_link_metadata_to_translation()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Movies").Row(new
|
||||
{
|
||||
Id = 5,
|
||||
Monitored = true,
|
||||
Title = "Title",
|
||||
CleanTitle = "CleanTitle",
|
||||
Status = 3,
|
||||
MinimumAvailability = 4,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Recommendations = new[] { 1 }.ToJson(),
|
||||
Runtime = 90,
|
||||
OriginalLanguage = 1,
|
||||
ProfileId = 1,
|
||||
MovieFileId = 0,
|
||||
Path = string.Format("/Movies/{0}", "Title"),
|
||||
TitleSlug = 123456,
|
||||
TmdbId = 132456,
|
||||
Added = DateTime.UtcNow,
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
LastInfoSync = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("MovieTranslations").Row(new
|
||||
{
|
||||
MovieId = 5,
|
||||
Title = "Some Trans",
|
||||
Language = 1
|
||||
});
|
||||
});
|
||||
|
||||
var metadata = db.Query<MovieMetadata207>("SELECT \"Id\", \"Title\", \"TmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
metadata.Should().HaveCount(1);
|
||||
metadata.First().TmdbId.Should().Be(132456);
|
||||
metadata.First().Title.Should().Be("Title");
|
||||
|
||||
var movies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"MovieTranslations\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_metadata_from_list_and_link_back()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ImportListMovies").Row(new
|
||||
{
|
||||
Title = "Title",
|
||||
Status = 3,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Runtime = 90,
|
||||
TmdbId = 123456,
|
||||
ListId = 4,
|
||||
Translations = new[] { new { } }.ToJson(),
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
});
|
||||
});
|
||||
|
||||
var metadata = db.Query<MovieMetadata207>("SELECT \"Id\", \"Title\", \"TmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
metadata.Should().HaveCount(1);
|
||||
metadata.First().TmdbId.Should().Be(123456);
|
||||
metadata.First().Title.Should().Be("Title");
|
||||
|
||||
var movies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"ImportListMovies\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_duplicate_metadata()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Movies").Row(new
|
||||
{
|
||||
Monitored = true,
|
||||
Title = "Title",
|
||||
CleanTitle = "CleanTitle",
|
||||
Status = 3,
|
||||
MinimumAvailability = 4,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Recommendations = new[] { 1 }.ToJson(),
|
||||
Runtime = 90,
|
||||
OriginalLanguage = 1,
|
||||
ProfileId = 1,
|
||||
MovieFileId = 0,
|
||||
Path = string.Format("/Movies/{0}", "Title"),
|
||||
TitleSlug = 123456,
|
||||
TmdbId = 123456,
|
||||
Added = DateTime.UtcNow,
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
LastInfoSync = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("ImportListMovies").Row(new
|
||||
{
|
||||
Title = "Title",
|
||||
Status = 3,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Runtime = 90,
|
||||
TmdbId = 123456,
|
||||
ListId = 4,
|
||||
Translations = new[] { new { } }.ToJson(),
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
});
|
||||
});
|
||||
|
||||
var metadata = db.Query<MovieMetadata207>("SELECT \"Id\", \"Title\", \"TmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
metadata.Should().HaveCount(1);
|
||||
metadata.First().TmdbId.Should().Be(123456);
|
||||
metadata.First().Title.Should().Be("Title");
|
||||
|
||||
var movies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"Movies\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
|
||||
var listMovies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"ImportListMovies\"");
|
||||
|
||||
listMovies.Should().HaveCount(1);
|
||||
listMovies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_duplicate_metadata_from_lists()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ImportListMovies").Row(new
|
||||
{
|
||||
Title = "Title",
|
||||
Overview = "Overview 1",
|
||||
Status = 3,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Runtime = 90,
|
||||
TmdbId = 123456,
|
||||
ListId = 4,
|
||||
Translations = new[] { new { } }.ToJson(),
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("ImportListMovies").Row(new
|
||||
{
|
||||
Title = "Title",
|
||||
Overview = "Overview 2",
|
||||
Status = 3,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Runtime = 90,
|
||||
TmdbId = 123456,
|
||||
ListId = 5,
|
||||
Translations = new[] { new { } }.ToJson(),
|
||||
Collection = new { Name = "Some Collection", TmdbId = 11 }.ToJson(),
|
||||
});
|
||||
});
|
||||
|
||||
var metadata = db.Query<MovieMetadata207>("SELECT \"Id\", \"Title\", \"TmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
metadata.Should().HaveCount(1);
|
||||
metadata.First().TmdbId.Should().Be(123456);
|
||||
metadata.First().Title.Should().Be("Title");
|
||||
|
||||
var listMovies = db.Query<Movie207>("SELECT \"Id\", \"MovieMetadataId\" FROM \"ImportListMovies\"");
|
||||
|
||||
listMovies.Should().HaveCount(2);
|
||||
listMovies.First().MovieMetadataId.Should().Be(metadata.First().Id);
|
||||
}
|
||||
}
|
||||
|
||||
public class MovieMetadata207
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
}
|
||||
|
||||
public class Movie207
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MovieMetadataId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,51 @@ namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
movies.First().CollectionTmdbId.Should().Be(collections.First().TmdbId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_collection_from_movie_without_name()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Movies").Row(new
|
||||
{
|
||||
Monitored = true,
|
||||
MinimumAvailability = 4,
|
||||
ProfileId = 1,
|
||||
MovieFileId = 0,
|
||||
MovieMetadataId = 1,
|
||||
Path = string.Format("/Movies/{0}", "Title"),
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("MovieMetadata").Row(new
|
||||
{
|
||||
Title = "Title",
|
||||
CleanTitle = "CleanTitle",
|
||||
Status = 3,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Recommendations = new[] { 1 }.ToJson(),
|
||||
Runtime = 90,
|
||||
OriginalTitle = "Title",
|
||||
CleanOriginalTitle = "CleanTitle",
|
||||
OriginalLanguage = 1,
|
||||
TmdbId = 132456,
|
||||
Collection = new { TmdbId = 11 }.ToJson(),
|
||||
LastInfoSync = DateTime.UtcNow,
|
||||
});
|
||||
});
|
||||
|
||||
var collections = db.Query<Collection208>("SELECT \"Id\", \"Title\", \"TmdbId\", \"Monitored\" FROM \"Collections\"");
|
||||
|
||||
collections.Should().HaveCount(1);
|
||||
collections.First().TmdbId.Should().Be(11);
|
||||
collections.First().Title.Should().Be("Collection 11");
|
||||
collections.First().Monitored.Should().BeFalse();
|
||||
|
||||
var movies = db.Query<Movie208>("SELECT \"Id\", \"CollectionTmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().CollectionTmdbId.Should().Be(collections.First().TmdbId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_duplicate_collection()
|
||||
{
|
||||
|
||||
66
src/NzbDrone.Core.Test/Files/Indexers/Nyaa/Nyaa2021.xml
Normal file
66
src/NzbDrone.Core.Test/Files/Indexers/Nyaa/Nyaa2021.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:nyaa="https://nyaa.si/xmlns/nyaa" version="2.0">
|
||||
<channel>
|
||||
<title>Nyaa - Home - Torrent File RSS</title>
|
||||
<description>RSS Feed for Home</description>
|
||||
<link>https://nyaa.si/</link>
|
||||
<atom:link href="https://nyaa.si/?page=rss" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<title>[Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv</title>
|
||||
<link>https://nyaa.si/download/1424896.torrent</link>
|
||||
<guid isPermaLink="true">https://nyaa.si/view/1424896</guid>
|
||||
<pubDate>Tue, 24 Aug 2021 22:18:46 -0000</pubDate>
|
||||
<nyaa:seeders>4</nyaa:seeders>
|
||||
<nyaa:leechers>3</nyaa:leechers>
|
||||
<nyaa:downloads>2</nyaa:downloads>
|
||||
<nyaa:infoHash>e8ca5e20eca876339f41c3d9e95ea66c1d7caaee</nyaa:infoHash>
|
||||
<nyaa:categoryId>1_3</nyaa:categoryId>
|
||||
<nyaa:category>Anime - Non-English-translated</nyaa:category>
|
||||
<nyaa:size>609.6 MiB</nyaa:size>
|
||||
<nyaa:comments>0</nyaa:comments>
|
||||
<nyaa:trusted>No</nyaa:trusted>
|
||||
<nyaa:remake>No</nyaa:remake>
|
||||
<description>
|
||||
<![CDATA[ <a href="https://nyaa.si/view/1424896">#1424896 | [Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv</a> | 609.6 MiB | Anime - Non-English-translated | E8CA5E20ECA876339F41C3D9E95EA66C1D7CAAEE ]]>
|
||||
</description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Macross Zero (BDRip 1920x1080p x265 HEVC TrueHD, FLAC 5.1+2.0)[sxales]</title>
|
||||
<link>https://nyaa.si/download/1424895.torrent</link>
|
||||
<guid isPermaLink="true">https://nyaa.si/view/1424895</guid>
|
||||
<pubDate>Tue, 24 Aug 2021 22:03:11 -0000</pubDate>
|
||||
<nyaa:seeders>23</nyaa:seeders>
|
||||
<nyaa:leechers>32</nyaa:leechers>
|
||||
<nyaa:downloads>17</nyaa:downloads>
|
||||
<nyaa:infoHash>26f37f26d5b3475b41a98dc575fabfa6f8d32a76</nyaa:infoHash>
|
||||
<nyaa:categoryId>1_2</nyaa:categoryId>
|
||||
<nyaa:category>Anime - English-translated</nyaa:category>
|
||||
<nyaa:size>5.7 GiB</nyaa:size>
|
||||
<nyaa:comments>2</nyaa:comments>
|
||||
<nyaa:trusted>No</nyaa:trusted>
|
||||
<nyaa:remake>No</nyaa:remake>
|
||||
<description>
|
||||
<![CDATA[ <a href="https://nyaa.si/view/1424895">#1424895 | Macross Zero (BDRip 1920x1080p x265 HEVC TrueHD, FLAC 5.1+2.0)[sxales]</a> | 5.7 GiB | Anime - English-translated | 26F37F26D5B3475B41A98DC575FABFA6F8D32A76 ]]>
|
||||
</description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Fumetsu no Anata e - 19 [WEBDL 1080p] Ukr DVO</title>
|
||||
<link>https://nyaa.si/download/1424887.torrent</link>
|
||||
<guid isPermaLink="true">https://nyaa.si/view/1424887</guid>
|
||||
<pubDate>Tue, 24 Aug 2021 21:23:06 -0000</pubDate>
|
||||
<nyaa:seeders>5</nyaa:seeders>
|
||||
<nyaa:leechers>4</nyaa:leechers>
|
||||
<nyaa:downloads>4</nyaa:downloads>
|
||||
<nyaa:infoHash>3e4300e24b39983802162877755aab4380bd137a</nyaa:infoHash>
|
||||
<nyaa:categoryId>1_3</nyaa:categoryId>
|
||||
<nyaa:category>Anime - Non-English-translated</nyaa:category>
|
||||
<nyaa:size>1.4 GiB</nyaa:size>
|
||||
<nyaa:comments>0</nyaa:comments>
|
||||
<nyaa:trusted>No</nyaa:trusted>
|
||||
<nyaa:remake>No</nyaa:remake>
|
||||
<description>
|
||||
<![CDATA[ <a href="https://nyaa.si/view/1424887">#1424887 | Fumetsu no Anata e - 19 [WEBDL 1080p] Ukr DVO</a> | 1.4 GiB | Anime - Non-English-translated | 3E4300E24B39983802162877755AAB4380BD137A ]]>
|
||||
</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -35,6 +35,11 @@
|
||||
"title": "Avengers: Endgame",
|
||||
"year": 2019,
|
||||
"Guid": []
|
||||
},
|
||||
{
|
||||
"type": "movie",
|
||||
"title": "Avengers",
|
||||
"year": 2005
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
public void should_return_ok_on_movie_imported_event()
|
||||
{
|
||||
GivenFolderExists(_downloadRootPath);
|
||||
var importEvent = new MovieImportedEvent(new LocalMovie(), new MovieFile(), new List<MovieFile>(), true, new DownloadClientItem());
|
||||
var importEvent = new MovieFileImportedEvent(new LocalMovie(), new MovieFile(), new List<MovieFile>(), true, new DownloadClientItem());
|
||||
|
||||
Subject.Check(importEvent).ShouldBeOk();
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.HistoryTests
|
||||
DownloadId = "abcd"
|
||||
};
|
||||
|
||||
Subject.Handle(new MovieImportedEvent(localMovie, movieFile, new List<MovieFile>(), true, downloadClientItem));
|
||||
Subject.Handle(new MovieFileImportedEvent(localMovie, movieFile, new List<MovieFile>(), true, downloadClientItem));
|
||||
|
||||
Mocker.GetMock<IHistoryRepository>()
|
||||
.Verify(v => v.Insert(It.Is<MovieHistory>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localMovie.Path))));
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_credit_items()
|
||||
public void should_delete_orphaned_collection_with_meta_but_no_movie_items()
|
||||
{
|
||||
var collection = Builder<MovieCollection>.CreateNew()
|
||||
.With(h => h.Id = 3)
|
||||
@@ -40,6 +40,27 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
|
||||
Db.Insert(movie);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_collection()
|
||||
{
|
||||
var collection = Builder<MovieCollection>.CreateNew()
|
||||
.With(h => h.Id = 3)
|
||||
.With(h => h.TmdbId = 123456)
|
||||
.With(h => h.Title = "Some Credit")
|
||||
.BuildNew();
|
||||
|
||||
Db.Insert(collection);
|
||||
|
||||
var movieMeta = Builder<MovieMetadata>.CreateNew().With(m => m.CollectionTmdbId = collection.TmdbId).BuildNew();
|
||||
Db.Insert(movieMeta);
|
||||
|
||||
var movie = Builder<Movie>.CreateNew().With(m => m.MovieMetadataId = movieMeta.Id).BuildNew();
|
||||
Db.Insert(movie);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||
.Returns(_movie);
|
||||
|
||||
Mocker.GetMock<IMovieTranslationService>()
|
||||
.Setup(s => s.GetAllTranslationsForMovie(It.IsAny<int>()))
|
||||
.Setup(s => s.GetAllTranslationsForMovieMetadata(It.IsAny<int>()))
|
||||
.Returns(new List<MovieTranslation>());
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
/* [Test]
|
||||
// Legacy Nyaa feed test
|
||||
|
||||
public void should_parse_recent_feed_from_Nyaa()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa.xml");
|
||||
@@ -53,6 +55,35 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Peers.Should().Be(2 + 1);
|
||||
torrentInfo.Seeders.Should().Be(1);
|
||||
}*/
|
||||
|
||||
[Test]
|
||||
public void should_parse_2021_recent_feed_from_Nyaa()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa2021.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(3);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("[Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://nyaa.si/download/1424896.torrent");
|
||||
torrentInfo.InfoUrl.Should().Be("https://nyaa.si/view/1424896");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("Tue, 24 Aug 2021 22:18:46"));
|
||||
torrentInfo.Size.Should().Be(639211930); //609.6 MiB
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Seeders.Should().Be(4);
|
||||
torrentInfo.Peers.Should().Be(3 + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<MovieImportedEvent>()), Times.Once());
|
||||
.Verify(v => v.PublishEvent(It.IsAny<MovieFileImportedEvent>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -8,8 +8,10 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Collections;
|
||||
using NzbDrone.Core.Movies.Commands;
|
||||
using NzbDrone.Core.Movies.Credits;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
@@ -19,6 +21,7 @@ namespace NzbDrone.Core.Test.MovieTests
|
||||
public class RefreshMovieServiceFixture : CoreTest<RefreshMovieService>
|
||||
{
|
||||
private MovieMetadata _movie;
|
||||
private MovieCollection _movieCollection;
|
||||
private Movie _existingMovie;
|
||||
|
||||
[SetUp]
|
||||
@@ -28,6 +31,9 @@ namespace NzbDrone.Core.Test.MovieTests
|
||||
.With(s => s.Status = MovieStatusType.Released)
|
||||
.Build();
|
||||
|
||||
_movieCollection = Builder<MovieCollection>.CreateNew()
|
||||
.Build();
|
||||
|
||||
_existingMovie = Builder<Movie>.CreateNew()
|
||||
.With(s => s.MovieMetadata.Value.Status = MovieStatusType.Released)
|
||||
.Build();
|
||||
@@ -40,9 +46,17 @@ namespace NzbDrone.Core.Test.MovieTests
|
||||
.Setup(s => s.Get(_movie.Id))
|
||||
.Returns(_movie);
|
||||
|
||||
Mocker.GetMock<IAddMovieCollectionService>()
|
||||
.Setup(v => v.AddMovieCollection(It.IsAny<MovieCollection>()))
|
||||
.Returns(_movieCollection);
|
||||
|
||||
Mocker.GetMock<IProvideMovieInfo>()
|
||||
.Setup(s => s.GetMovieInfo(It.IsAny<int>()))
|
||||
.Callback<int>((i) => { throw new MovieNotFoundException(i); });
|
||||
|
||||
Mocker.GetMock<IRootFolderService>()
|
||||
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
|
||||
.Returns(string.Empty);
|
||||
}
|
||||
|
||||
private void GivenNewMovieInfo(MovieMetadata movie)
|
||||
|
||||
@@ -64,6 +64,11 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
TestLogger.Info("OnRename was called");
|
||||
}
|
||||
|
||||
public override void OnMovieAdded(Movie movie)
|
||||
{
|
||||
TestLogger.Info("OnMovieAdded was called");
|
||||
}
|
||||
|
||||
public override void OnMovieFileDelete(MovieFileDeleteMessage message)
|
||||
{
|
||||
TestLogger.Info("OnMovieFileDelete was called");
|
||||
@@ -117,6 +122,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
notification.SupportsOnDownload.Should().BeTrue();
|
||||
notification.SupportsOnUpgrade.Should().BeTrue();
|
||||
notification.SupportsOnRename.Should().BeTrue();
|
||||
notification.SupportsOnMovieAdded.Should().BeTrue();
|
||||
notification.SupportsOnMovieDelete.Should().BeTrue();
|
||||
notification.SupportsOnMovieFileDelete.Should().BeTrue();
|
||||
notification.SupportsOnMovieFileDeleteForUpgrade.Should().BeTrue();
|
||||
@@ -133,6 +139,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
notification.SupportsOnDownload.Should().BeFalse();
|
||||
notification.SupportsOnUpgrade.Should().BeFalse();
|
||||
notification.SupportsOnRename.Should().BeFalse();
|
||||
notification.SupportsOnMovieAdded.Should().BeFalse();
|
||||
notification.SupportsOnMovieDelete.Should().BeFalse();
|
||||
notification.SupportsOnMovieFileDelete.Should().BeFalse();
|
||||
notification.SupportsOnMovieFileDeleteForUpgrade.Should().BeFalse();
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
.Returns(new List<CustomFormat>());
|
||||
|
||||
Mocker.GetMock<IMovieTranslationService>()
|
||||
.Setup(v => v.GetAllTranslationsForMovie(It.IsAny<int>()))
|
||||
.Setup(v => v.GetAllTranslationsForMovieMetadata(It.IsAny<int>()))
|
||||
.Returns(_movieTranslations);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
|
||||
[TestCase("Movie.Title.1994.Dublado.1080p.XviD-LOL")]
|
||||
[TestCase("Movie.Title.2.2019.1080p.Bluray.Dublado.WWW.TPF.GRATIS")]
|
||||
[TestCase("Movie.Title.2014.1080p.Bluray.Brazilian.WWW.TPF.GRATIS")]
|
||||
public void should_parse_language_brazilian_portuguese(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||
|
||||
@@ -67,6 +67,35 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(title);
|
||||
}
|
||||
|
||||
[TestCase("[MTBB] Kimi no Na wa. (2016) v2 [97681524].mkv", "Kimi no Na wa", "MTBB", 2016)]
|
||||
[TestCase("[sam] Toward the Terra (1980) [BD 1080p TrueHD].mkv", "Toward the Terra", "sam", 1980)]
|
||||
public void should_parse_anime_movie_title(string postTitle, string title, string releaseGroup, int year)
|
||||
{
|
||||
ParsedMovieInfo movie = Parser.Parser.ParseMovieTitle(postTitle);
|
||||
using (new AssertionScope())
|
||||
{
|
||||
movie.PrimaryMovieTitle.Should().Be(title);
|
||||
movie.ReleaseGroup.Should().Be(releaseGroup);
|
||||
movie.Year.Should().Be(year);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("[Arid] Cowboy Bebop - Knockin' on Heaven's Door v2 [00F4CDA0].mkv", "Cowboy Bebop - Knockin' on Heaven's Door", "Arid")]
|
||||
[TestCase("[Baws] Evangelion 1.11 - You Are (Not) Alone v2 (1080p BD HEVC FLAC) [BF42B1C8].mkv", "Evangelion 1 11 - You Are (Not) Alone", "Baws")]
|
||||
[TestCase("[Arid] 5 Centimeters per Second (BDRip 1920x1080 Hi10 FLAC) [FD8B6FF2].mkv", "5 Centimeters per Second", "Arid")]
|
||||
[TestCase("[Baws] Evangelion 2.22 - You Can (Not) Advance (1080p BD HEVC FLAC) [56E7A5B8].mkv", "Evangelion 2 22 - You Can (Not) Advance", "Baws")]
|
||||
[TestCase("[sam] Goblin Slayer - Goblin's Crown [BD 1080p FLAC] [CD298D48].mkv", "Goblin Slayer - Goblin's Crown", "sam")]
|
||||
[TestCase("[Kulot] Violet Evergarden Gaiden Eien to Jidou Shuki Ningyou [Dual-Audio][BDRip 1920x804 HEVC FLACx2] [91FC62A8].mkv", "Violet Evergarden Gaiden Eien to Jidou Shuki Ningyou", "Kulot")]
|
||||
public void should_parse_anime_movie_title_without_year(string postTitle, string title, string releaseGroup)
|
||||
{
|
||||
ParsedMovieInfo movie = Parser.Parser.ParseMovieTitle(postTitle);
|
||||
using (new AssertionScope())
|
||||
{
|
||||
movie.PrimaryMovieTitle.Should().Be(title);
|
||||
movie.ReleaseGroup.Should().Be(releaseGroup);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("Movie.Aufbruch.nach.Pandora.Extended.2009.German.DTS.720p.BluRay.x264-SoW", "Movie Aufbruch nach Pandora", "Extended", 2009)]
|
||||
[TestCase("Drop.Movie.1994.German.AC3D.DL.720p.BluRay.x264-KLASSiGERHD", "Drop Movie", "", 1994)]
|
||||
[TestCase("Kick.Movie.2.2013.German.DTS.DL.720p.BluRay.x264-Pate", "Kick Movie 2", "", 2013)]
|
||||
|
||||
@@ -41,6 +41,14 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
ParseAndVerifyQuality(title, Source.TELESYNC, proper, Resolution.R720p);
|
||||
}
|
||||
|
||||
[TestCase("Movie Name 2018 NEW PROPER 720p HD-CAM X264 HQ-CPG", true)]
|
||||
[TestCase("Movie Name (2022) 1080p HQCAM ENG x264 AAC - QRips", false)]
|
||||
[TestCase("Movie Name (2018) 720p Hindi HQ CAMrip x264 AAC 1.4GB", false)]
|
||||
public void should_parse_cam(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.CAM, proper, Resolution.Unknown);
|
||||
}
|
||||
|
||||
[TestCase("S07E23 .avi ", false)]
|
||||
[TestCase("Movie Name S02E01 HDTV XviD 2HD", false)]
|
||||
[TestCase("Movie Name S05E11 PROPER HDTV XviD 2HD", true)]
|
||||
@@ -189,6 +197,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Title.2020.MULTi.1080p.WEB.H264-ALLDAYiN (S:285/L:11)", false)]
|
||||
[TestCase("Movie Title (2020) MULTi WEB 1080p x264-JiHEFF (S:317/L:28)", false)]
|
||||
[TestCase("Movie.Titles.2020.1080p.NF.WEB.DD2.0.x264-SNEAkY", false)]
|
||||
[TestCase("The.Movie.2022.NORDiC.1080p.DV.HDR.WEB.H 265-NiDHUG", false)]
|
||||
public void should_parse_webdl1080p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.WEBDL, proper, Resolution.R1080p);
|
||||
@@ -207,6 +216,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Name.2016.03.14.2160p.WEB.PROPER.h264-spamTV", true)]
|
||||
[TestCase("[HorribleSubs] Movie Title! 2018 [Web][MKV][h264][2160p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
||||
[TestCase("Movie Name 2020 WEB-DL 4K H265 10bit HDR DDP5.1 Atmos-PTerWEB", false)]
|
||||
[TestCase("The.Movie.2022.NORDiC.2160p.DV.HDR.WEB.H.265-NiDHUG", false)]
|
||||
public void should_parse_webdl2160p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.WEBDL, proper, Resolution.R2160p);
|
||||
@@ -261,6 +271,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie.Name.2016.German.DTS.DL.1080p.UHDBD.x265-TDO.mkv", false)]
|
||||
[TestCase("Movie.Name.2021.1080p.BDLight.x265-AVCDVD", false)]
|
||||
[TestCase("Random.Title.2010.1080p.HD.DVD.AVC.DDP.5.1-GRouP", false)]
|
||||
[TestCase("Movie.Title.2012.German.DL.1080p.UHD2BD.x264-QfG", false)]
|
||||
public void should_parse_bluray1080p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.BLURAY, proper, Resolution.R1080p);
|
||||
|
||||
@@ -99,6 +99,11 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Crappy Anime Movie Name 2017 [RH] [Blu-ray][MKV][h264 10-bit][1080p][FLAC 5.1][Dual Audio][Softsubs (RH)]", "RH")]
|
||||
[TestCase("Yet Another Anime Movie 2012 [Kametsu] [Blu-ray][MKV][h264 10-bit][1080p][FLAC 5.1][Dual Audio][Softsubs (Kametsu)]", "Kametsu")]
|
||||
[TestCase("Another.Anime.Film.Name.2016.JPN.Blu-Ray.Remux.AVC.DTS-MA.BluDragon", "BluDragon")]
|
||||
[TestCase("A Movie in the Name (1964) (1080p BluRay x265 r00t)", "r00t")]
|
||||
[TestCase("Movie Title (2022) (2160p ATV WEB-DL Hybrid H265 DV HDR DDP Atmos 5.1 English - HONE)", "HONE")]
|
||||
[TestCase("Movie Title (2009) (2160p PMTP WEB-DL Hybrid H265 DV HDR10+ DDP Atmos 5.1 English - HONE)", "HONE")]
|
||||
[TestCase("Why.Cant.You.Use.Normal.Characters.2021.2160p.UHD.HDR10+.BluRay.TrueHD.Atmos.7.1.x265-ZØNEHD", "ZØNEHD")]
|
||||
[TestCase("Movie.Should.Not.Use.Dots.2022.1080p.BluRay.x265.10bit.Tigole", "Tigole")]
|
||||
public void should_parse_exception_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user