mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7de7e83c5b | |||
| b7a46bedb0 | |||
| 0925769377 | |||
| 72244362fe | |||
| c6526c34e9 | |||
| efa2913dbc | |||
| 35c22a4ffa | |||
| 66d96e21da | |||
| 36d4e9e6cd | |||
| 7189d7b15c | |||
| 6e80113987 | |||
| bb8a0dda63 | |||
| 525ed65687 | |||
| 3fbccc6af3 | |||
| 8e10eecfac | |||
| a3b1512552 | |||
| d375b5ffbe | |||
| 884abc0368 | |||
| f8da7aae03 | |||
| c165118d4d | |||
| b3dd571a92 | |||
| dd900eb739 | |||
| 66aae0c91c | |||
| d888a0a2b3 | |||
| cb5416a18c | |||
| 7977e0be05 | |||
| cd836fef38 | |||
| b0bfbe767c | |||
| 528b93dabe | |||
| 1edcbee5e1 | |||
| 8853dced9f | |||
| c7aa1bae5e | |||
| 405ae77070 | |||
| 6236bc9b4f | |||
| 743c977e5b | |||
| c0e5646f07 | |||
| 10094b4e66 | |||
| d923406f08 | |||
| 69a9c72286 | |||
| 55b9477a01 | |||
| 6b81f92137 | |||
| 3ceda1bcda | |||
| f1f1921517 | |||
| af0c96538a | |||
| 3d52f45b6a | |||
| d4715f119d | |||
| d58135bf17 | |||
| b452c10da3 | |||
| f6b364725d | |||
| 99f6be3f3d | |||
| c2ac49a873 | |||
| 0e24a3e8bc | |||
| 18032cc83b | |||
| 927eb38945 | |||
| 5fac348613 | |||
| 7ba9603449 | |||
| e36de8ab8d | |||
| f8704a1655 | |||
| f507d5154e | |||
| 5f03e7142a | |||
| c0ebbee7c9 | |||
| 4051cf3d80 | |||
| 9876ed64e2 | |||
| 2f26974ecc | |||
| 25f66a3029 | |||
| 0e25b2708c | |||
| 410870d21e | |||
| a64d931904 | |||
| f0a9e76cfc | |||
| 6f23c465ee | |||
| af60cca9ae | |||
| d34d23a052 | |||
| 0a0da42543 | |||
| e5419f6f06 | |||
| 88d9c08f1a | |||
| 6b4259757c | |||
| f1d7c56d94 | |||
| c81b2e80ee | |||
| 5efefd804b | |||
| 38f9543526 | |||
| aae68e681e | |||
| 1d21bbf78f | |||
| 99c3c8ce5b | |||
| 85171e40a5 | |||
| 86b656d323 | |||
| 5ae5d1043a | |||
| b801aa0935 | |||
| b2b5aa1f79 | |||
| 8c6ba9a543 | |||
| 4e024c51d3 | |||
| e4106f0ede | |||
| 9032ac20ff |
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
|
|||||||
### License
|
### License
|
||||||
|
|
||||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
* Copyright 2010-2024
|
* Copyright 2010-2025
|
||||||
|
|||||||
+8
-8
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.16.2'
|
majorVersion: '5.19.2'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
@@ -1116,20 +1116,20 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'radarr'
|
organization: 'radarr'
|
||||||
scannerMode: 'CLI'
|
scannerMode: 'cli'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'Radarr_Radarr.UI'
|
cliProjectKey: 'Radarr_Radarr.UI'
|
||||||
cliProjectName: 'RadarrUI'
|
cliProjectName: 'RadarrUI'
|
||||||
cliProjectVersion: '$(radarrVersion)'
|
cliProjectVersion: '$(radarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
@@ -1205,12 +1205,12 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'radarr'
|
organization: 'radarr'
|
||||||
scannerMode: 'MSBuild'
|
scannerMode: 'dotnet'
|
||||||
projectKey: 'Radarr_Radarr'
|
projectKey: 'Radarr_Radarr'
|
||||||
projectName: 'Radarr'
|
projectName: 'Radarr'
|
||||||
projectVersion: '$(radarrVersion)'
|
projectVersion: '$(radarrVersion)'
|
||||||
@@ -1223,7 +1223,7 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5.3.11
|
- task: reportgenerator@5.3.11
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
ARCHITECTURE="${2:-x64}"
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-x64"
|
RUNTIME="win-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-x64"
|
RUNTIME="linux-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-x64"
|
RUNTIME="osx-$ARCHITECTURE"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
|||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
|
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.39'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
|||||||
indexer,
|
indexer,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
movieMatchType,
|
movieMatchType,
|
||||||
|
releaseSource,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
nzbInfoUrl,
|
nzbInfoUrl,
|
||||||
downloadClient,
|
downloadClient,
|
||||||
@@ -53,6 +54,31 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
|||||||
|
|
||||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||||
|
|
||||||
|
let releaseSourceMessage = '';
|
||||||
|
|
||||||
|
switch (releaseSource) {
|
||||||
|
case 'Unknown':
|
||||||
|
releaseSourceMessage = translate('Unknown');
|
||||||
|
break;
|
||||||
|
case 'Rss':
|
||||||
|
releaseSourceMessage = translate('Rss');
|
||||||
|
break;
|
||||||
|
case 'Search':
|
||||||
|
releaseSourceMessage = translate('Search');
|
||||||
|
break;
|
||||||
|
case 'UserInvokedSearch':
|
||||||
|
releaseSourceMessage = translate('UserInvokedSearch');
|
||||||
|
break;
|
||||||
|
case 'InteractiveSearch':
|
||||||
|
releaseSourceMessage = translate('InteractiveSearch');
|
||||||
|
break;
|
||||||
|
case 'ReleasePush':
|
||||||
|
releaseSourceMessage = translate('ReleasePush');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
releaseSourceMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -88,6 +114,14 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{releaseSource ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseSource')}
|
||||||
|
data={releaseSourceMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{nzbInfoUrl ? (
|
{nzbInfoUrl ? (
|
||||||
<span>
|
<span>
|
||||||
<DescriptionListItemTitle>
|
<DescriptionListItemTitle>
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ ImportMovieRow.propTypes = {
|
|||||||
selectedMovie: PropTypes.object,
|
selectedMovie: PropTypes.object,
|
||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
queued: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ImportMovieSelectMovie extends Component {
|
|||||||
id={this._buttonId}
|
id={this._buttonId}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
component="div"
|
component="div"
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
@@ -255,7 +255,7 @@ class ImportMovieSelectMovie extends Component {
|
|||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<ImportMovieSearchResultConnector
|
<ImportMovieSearchResultConnector
|
||||||
key={item.tvdbId}
|
key={item.tmdbId}
|
||||||
tmdbId={item.tmdbId}
|
tmdbId={item.tmdbId}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
year={item.year}
|
year={item.year}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
||||||
import Metadata from 'typings/Metadata';
|
import Metadata from 'typings/Metadata';
|
||||||
|
|
||||||
interface MetadataAppState extends AppSectionProviderState<Metadata> {}
|
type MetadataAppState = AppSectionProviderState<Metadata>;
|
||||||
|
|
||||||
export default MetadataAppState;
|
export default MetadataAppState;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import MovieCredit from 'typings/MovieCredit';
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
|
||||||
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
type MovieCreditAppState = AppSectionState<MovieCredit>;
|
||||||
|
|
||||||
export default MovieCreditAppState;
|
export default MovieCreditAppState;
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ export interface NamingAppState
|
|||||||
extends AppSectionItemState<NamingConfig>,
|
extends AppSectionItemState<NamingConfig>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface NamingExamplesAppState
|
export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
|
||||||
extends AppSectionItemState<NamingExample> {}
|
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { ComponentPropsWithoutRef } from 'react';
|
import React, { ComponentPropsWithoutRef } from 'react';
|
||||||
import styles from './TableRowCell.css';
|
import styles from './TableRowCell.css';
|
||||||
|
|
||||||
export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {}
|
export type TableRowCellProps = ComponentPropsWithoutRef<'td'>;
|
||||||
|
|
||||||
export default function TableRowCell({
|
export default function TableRowCell({
|
||||||
className = styles.cell,
|
className = styles.cell,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function TraktRating(props: TraktRatingProps) {
|
|||||||
const { ratings, iconSize = 14, hideIcon = false } = props;
|
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||||
|
|
||||||
const traktImage =
|
const traktImage =
|
||||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
|
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICM5ZjQyYzY7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPGcgaWQ9Il94MkRfLXByb2R1Y3Rpb24iPgogICAgPGcgaWQ9ImxvZ29tYXJrLmNpcmNsZS5jb2xvciI+CiAgICAgIDxwYXRoIGlkPSJiYWNrZ3JvdW5kIiBjbGFzcz0iY2xzLTEiIGQ9Ik00OCwyNGMwLDYuNjItMi42OSwxMi42Mi03LjAzLDE2Ljk3LTQuMzQsNC4zNC0xMC4zNCw3LjAzLTE2Ljk3LDcuMDNDMTAuNzUsNDgsMCwzNy4yNSwwLDI0YzAtNi42MywyLjY5LTEyLjYzLDcuMDMtMTYuOTdDMTEuMzcsMi42OCwxNy4zNywwLDI0LDBzMTIuNjMsMi42OCwxNi45Nyw3LjAzYy4xNC4xNC4yNy4yOC40LjQyLjQ4LjUuOTQsMS4wMiwxLjM3LDEuNTYuMjEuMjYuNDEuNTIuNi43OS40My41Ny44MiwxLjE2LDEuMTgsMS43Ni4xOC4yOS4zNS41OC41MS44Ny4zNS42NC42OCwxLjI5Ljk2LDEuOTcsMS4zLDIuOTQsMi4wMSw2LjE4LDIuMDEsOS42WiIvPgogICAgICA8ZyBpZD0iY2hlY2tib3giPgogICAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEyLjkzLDE4LjY3bC0xLjQ3LDEuNDYsMTQuNCwxNC40LDEuNDctMS40Ny00LjMyLTQuMzEsMTkuNzMtMTkuNzRjLS40My0uNTQtLjg5LTEuMDYtMS4zNy0xLjU2bC0xOS44MywxOS44My04LjYxLTguNjFaTTI4LjAyLDMyLjM3bDEuNDYtMS40Ni0yLjE1LTIuMTYsMTcuMTktMTcuMTljLS4zNi0uNi0uNzUtMS4xOS0xLjE4LTEuNzZsLTE4Ljk0LDE4Ljk1LDMuNjIsMy42MlpNMzAuMTgsMzAuMjFsMTUuODEtMTUuODFjLS4yOC0uNjgtLjYxLTEuMzMtLjk2LTEuOTdsLTE2LjMyLDE2LjMyLDEuNDcsMS40NlpNMTMuNjIsMTcuOTdsNy45Miw3LjkyLDEuNDctMS40Ny03LjkyLTcuOTItMS40NywxLjQ3Wk0yNS4xNywyMi4yN2wtNy45Mi03LjkyLTEuNDcsMS40Nyw3LjkyLDcuOTIsMS40Ny0xLjQ3Wk0yNCw0MS4zMmMtOS41NSwwLTE3LjMyLTcuNzctMTcuMzItMTcuMzJTMTQuNDUsNi42NywyNCw2LjY3YzIuNiwwLDUuMTEuNTYsNy40NCwxLjY4bC44OS0xLjg3Yy0yLjYxLTEuMjUtNS40Mi0xLjg4LTguMzMtMS44OEMxMy4zMSw0LjYsNC42MSwxMy4zLDQuNjEsMjRzOC43LDE5LjQsMTkuNCwxOS40YzcuNjQsMCwxNC41OS00LjUxLDE3LjcxLTExLjQ4bC0xLjg5LS44NWMtMi43OSw2LjIzLTksMTAuMjYtMTUuODIsMTAuMjZaIi8+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg==';
|
||||||
|
|
||||||
const { value = 0, votes = 0 } = ratings.trakt;
|
const { value = 0, votes = 0 } = ratings.trakt;
|
||||||
|
|
||||||
|
|||||||
@@ -85,10 +85,16 @@ $hoverScale: 1.05;
|
|||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overviewContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 1 1000px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
composes: link;
|
composes: link;
|
||||||
|
|
||||||
flex: 0 1 1000px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface CssExports {
|
|||||||
'link': string;
|
'link': string;
|
||||||
'lists': string;
|
'lists': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
|
'overviewContainer': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
'posterContainer': string;
|
'posterContainer': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
|||||||
@@ -133,14 +133,20 @@ class DiscoverMovieOverview extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MoviePoster
|
<Link
|
||||||
className={styles.poster}
|
className={styles.link}
|
||||||
style={elementStyle}
|
style={elementStyle}
|
||||||
images={images}
|
{...linkProps}
|
||||||
size={250}
|
>
|
||||||
lazy={false}
|
<MoviePoster
|
||||||
overflow={true}
|
className={styles.poster}
|
||||||
/>
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -242,11 +248,13 @@ class DiscoverMovieOverview extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div className={styles.overview}>
|
<div className={styles.overviewContainer}>
|
||||||
<TextTruncate
|
<Link className={styles.overview} {...linkProps}>
|
||||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
<TextTruncate
|
||||||
text={overview}
|
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||||
/>
|
text={overview}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DiscoverMovieOverviewInfo
|
<DiscoverMovieOverviewInfo
|
||||||
@@ -255,7 +263,6 @@ class DiscoverMovieOverview extends Component {
|
|||||||
{...overviewOptions}
|
{...overviewOptions}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import TmdbRating from 'Components/TmdbRating';
|
|||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import TraktRating from 'Components/TraktRating';
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
@@ -753,11 +753,15 @@ class MovieDetails extends Component {
|
|||||||
<InteractiveImportModal
|
<InteractiveImportModal
|
||||||
isOpen={isInteractiveImportModalOpen}
|
isOpen={isInteractiveImportModalOpen}
|
||||||
movieId={id}
|
movieId={id}
|
||||||
modalTitle={translate('ManageFiles')}
|
title={title}
|
||||||
folder={path}
|
folder={path}
|
||||||
|
initialSortKey="relativePath"
|
||||||
|
initialSortDirection={sortDirections.ASCENDING}
|
||||||
|
showMovie={false}
|
||||||
allowMovieChange={false}
|
allowMovieChange={false}
|
||||||
showFilterExistingFiles={true}
|
showDelete={true}
|
||||||
showImportMode={false}
|
showImportMode={false}
|
||||||
|
modalTitle={translate('ManageFiles')}
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,19 @@ function MovieDetailsLinks(props: MovieDetailsLinksProps) {
|
|||||||
MDBList
|
MDBList
|
||||||
</Label>
|
</Label>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
to={`https://www.blu-ray.com/search/?quicksearch=1&quicksearch_keyword=${imdbId}§ion=theatrical`}
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
className={styles.linkLabel}
|
||||||
|
kind={kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
Blu-ray
|
||||||
|
</Label>
|
||||||
|
</Link>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import MediaInfoProps from 'typings/MediaInfo';
|
||||||
|
import formatBitrate from 'Utilities/Number/formatBitrate';
|
||||||
|
import getEntries from 'Utilities/Object/getEntries';
|
||||||
|
|
||||||
|
function MediaInfo(props: MediaInfoProps) {
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
{getEntries(props).map(([key, value]) => {
|
||||||
|
const title = key
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/^./, (str) => str.toUpperCase());
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'audioBitrate' || key === 'videoBitrate') {
|
||||||
|
return (
|
||||||
|
<DescriptionListItem
|
||||||
|
key={key}
|
||||||
|
title={title}
|
||||||
|
data={
|
||||||
|
<span title={value.toString()}>{formatBitrate(value)}</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DescriptionListItem key={key} title={title} data={value} />;
|
||||||
|
})}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaInfo;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
|
|
||||||
function MediaInfoPopover(props) {
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
{
|
|
||||||
Object.keys(props).map((key) => {
|
|
||||||
const title = key
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/^./, (str) => str.toUpperCase());
|
|
||||||
|
|
||||||
const value = props[key];
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionListItem
|
|
||||||
key={key}
|
|
||||||
title={title}
|
|
||||||
data={props[key]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MediaInfoPopover;
|
|
||||||
@@ -14,7 +14,7 @@ import MovieFormats from 'Movie/MovieFormats';
|
|||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
import FileEditModal from 'MovieFile/Edit/FileEditModal';
|
import FileEditModal from 'MovieFile/Edit/FileEditModal';
|
||||||
import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
|
import MediaInfo from 'MovieFile/MediaInfo';
|
||||||
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
@@ -224,7 +224,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.audio}
|
className={styles.audio}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.AUDIO}
|
type={mediaInfoTypes.AUDIO}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -238,7 +238,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.audioLanguages}
|
className={styles.audioLanguages}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -252,7 +252,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.subtitles}
|
className={styles.subtitles}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.SUBTITLES}
|
type={mediaInfoTypes.SUBTITLES}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -266,7 +266,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.video}
|
className={styles.video}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.VIDEO}
|
type={mediaInfoTypes.VIDEO}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -280,7 +280,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.videoDynamicRangeType}
|
className={styles.videoDynamicRangeType}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MediaInfoPopover from './Editor/MediaInfoPopover';
|
import MediaInfo from './Editor/MediaInfo';
|
||||||
|
|
||||||
function FileDetailsModal(props) {
|
function FileDetailsModal(props) {
|
||||||
const {
|
const {
|
||||||
@@ -31,7 +31,7 @@ function FileDetailsModal(props) {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<MediaInfoPopover {...mediaInfo} />
|
<MediaInfo {...mediaInfo} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import getLanguageName from 'Utilities/String/getLanguageName';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import * as mediaInfoTypes from './mediaInfoTypes';
|
|
||||||
|
|
||||||
function formatLanguages(languages) {
|
|
||||||
if (!languages) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitLanguages = _.uniq(languages.split('/')).map((l) => {
|
|
||||||
const simpleLanguage = l.split('_')[0];
|
|
||||||
|
|
||||||
if (simpleLanguage === 'und') {
|
|
||||||
return translate('Unknown');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getLanguageName(simpleLanguage);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (splitLanguages.length > 3) {
|
|
||||||
return (
|
|
||||||
<span title={splitLanguages.join(', ')}>
|
|
||||||
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{splitLanguages.join(', ')}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MediaInfo(props) {
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
audioChannels,
|
|
||||||
audioCodec,
|
|
||||||
audioLanguages,
|
|
||||||
subtitles,
|
|
||||||
videoCodec,
|
|
||||||
videoDynamicRangeType
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.AUDIO) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
audioCodec ? audioCodec : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
audioCodec && audioChannels ? ' - ' : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
audioChannels ? audioChannels.toFixed(1) : ''
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
|
|
||||||
return formatLanguages(audioLanguages);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.SUBTITLES) {
|
|
||||||
return formatLanguages(subtitles);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.VIDEO) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{videoCodec}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{videoDynamicRangeType}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaInfo.propTypes = {
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
audioChannels: PropTypes.number,
|
|
||||||
audioCodec: PropTypes.string,
|
|
||||||
audioLanguages: PropTypes.string,
|
|
||||||
subtitles: PropTypes.string,
|
|
||||||
videoCodec: PropTypes.string,
|
|
||||||
videoDynamicRangeType: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MediaInfo;
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import useMovieFile from './useMovieFile';
|
||||||
|
|
||||||
|
function formatLanguages(languages: string | undefined) {
|
||||||
|
if (!languages) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitLanguages = [...new Set(languages.split('/'))].map((l) => {
|
||||||
|
const simpleLanguage = l.split('_')[0];
|
||||||
|
|
||||||
|
if (simpleLanguage === 'und') {
|
||||||
|
return translate('Unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLanguageName(simpleLanguage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (splitLanguages.length > 3) {
|
||||||
|
return (
|
||||||
|
<span title={splitLanguages.join(', ')}>
|
||||||
|
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2}{' '}
|
||||||
|
more
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{splitLanguages.join(', ')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MediaInfoType =
|
||||||
|
| 'audio'
|
||||||
|
| 'audioLanguages'
|
||||||
|
| 'subtitles'
|
||||||
|
| 'video'
|
||||||
|
| 'videoDynamicRangeType';
|
||||||
|
|
||||||
|
interface MediaInfoProps {
|
||||||
|
movieFileId?: number;
|
||||||
|
type: MediaInfoType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MediaInfo({ movieFileId, type }: MediaInfoProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
|
||||||
|
if (!movieFile?.mediaInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
audioChannels,
|
||||||
|
audioCodec,
|
||||||
|
audioLanguages,
|
||||||
|
subtitles,
|
||||||
|
videoCodec,
|
||||||
|
videoDynamicRangeType,
|
||||||
|
} = movieFile.mediaInfo;
|
||||||
|
|
||||||
|
if (type === 'audio') {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{audioCodec ? audioCodec : ''}
|
||||||
|
|
||||||
|
{audioCodec && audioChannels ? ' - ' : ''}
|
||||||
|
|
||||||
|
{audioChannels ? audioChannels.toFixed(1) : ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'audioLanguages') {
|
||||||
|
return formatLanguages(audioLanguages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'subtitles') {
|
||||||
|
return formatLanguages(subtitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'video') {
|
||||||
|
return <span>{videoCodec}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'videoDynamicRangeType') {
|
||||||
|
return <span>{videoDynamicRangeType}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaInfo;
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
|
||||||
import MediaInfo from './MediaInfo';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieFileSelector(),
|
|
||||||
(movieFile) => {
|
|
||||||
if (movieFile) {
|
|
||||||
return {
|
|
||||||
...movieFile.mediaInfo
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MediaInfo);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
|
||||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieFileSelector(),
|
|
||||||
(movieFile) => {
|
|
||||||
return {
|
|
||||||
languages: movieFile ? movieFile.languages : undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieLanguages);
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
|
import useMovieFile from './useMovieFile';
|
||||||
|
|
||||||
|
interface MovieFileLanguagesProps {
|
||||||
|
movieFileId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieFileLanguages({ movieFileId }: MovieFileLanguagesProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
|
||||||
|
return <MovieLanguages languages={movieFile?.languages ?? []} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieFileLanguages;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createMovieFileSelector(movieFileId?: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.movieFiles.items,
|
||||||
|
(movieFiles) => {
|
||||||
|
return movieFiles.find(({ id }) => id === movieFileId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMovieFile(movieFileId: number | undefined) {
|
||||||
|
return useSelector(createMovieFileSelector(movieFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMovieFile;
|
||||||
+2
-2
@@ -55,10 +55,10 @@ function EditSpecificationModalContent(props) {
|
|||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
color: var(--helpTextColor);
|
color: var(--helpTextColor);
|
||||||
|
|
||||||
.icon {
|
.identifier {
|
||||||
margin-top: 3px;
|
margin-top: 8px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'footNote': string;
|
'footNote': string;
|
||||||
'groups': string;
|
'groups': string;
|
||||||
'icon': string;
|
'identifier': string;
|
||||||
'namingSelect': string;
|
'namingSelect': string;
|
||||||
'namingSelectContainer': string;
|
'namingSelectContainer': string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
|
|||||||
import FieldSet from 'Components/FieldSet';
|
import FieldSet from 'Components/FieldSet';
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
@@ -10,7 +9,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { icons, sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
import NamingConfig from 'typings/Settings/NamingConfig';
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import NamingOption from './NamingOption';
|
import NamingOption from './NamingOption';
|
||||||
@@ -88,32 +87,32 @@ const fileNameTokens = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const movieTokens = [
|
const movieTokens = [
|
||||||
{ token: '{Movie Title}', example: "Movie's Title", footNote: true },
|
{ token: '{Movie Title}', example: "Movie's Title", footNotes: '1' },
|
||||||
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: true },
|
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNotes: '1' },
|
||||||
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: true },
|
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNotes: '1' },
|
||||||
{
|
{
|
||||||
token: '{Movie CleanTitle:DE}',
|
token: '{Movie CleanTitle:DE}',
|
||||||
example: 'Titel des Films',
|
example: 'Titel des Films',
|
||||||
footNote: true,
|
footNotes: '1',
|
||||||
},
|
},
|
||||||
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNote: true },
|
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNotes: '1' },
|
||||||
{
|
{
|
||||||
token: '{Movie CleanTitleThe}',
|
token: '{Movie CleanTitleThe}',
|
||||||
example: 'Movies Title, The',
|
example: 'Movies Title, The',
|
||||||
footNote: true,
|
footNotes: '1',
|
||||||
},
|
},
|
||||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: true },
|
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNotes: '1' },
|
||||||
{
|
{
|
||||||
token: '{Movie CleanOriginalTitle}',
|
token: '{Movie CleanOriginalTitle}',
|
||||||
example: 'Τίτλος ταινίας',
|
example: 'Τίτλος ταινίας',
|
||||||
footNote: true,
|
footNotes: '1',
|
||||||
},
|
},
|
||||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||||
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
||||||
{
|
{
|
||||||
token: '{Movie Collection}',
|
token: '{Movie Collection}',
|
||||||
example: 'The Movie Collection',
|
example: 'The Movie Collection',
|
||||||
footNote: true,
|
footNotes: '1',
|
||||||
},
|
},
|
||||||
{ token: '{Movie Certification}', example: 'R' },
|
{ token: '{Movie Certification}', example: 'R' },
|
||||||
{ token: '{Release Year}', example: '2009' },
|
{ token: '{Release Year}', example: '2009' },
|
||||||
@@ -131,12 +130,21 @@ const qualityTokens = [
|
|||||||
|
|
||||||
const mediaInfoTokens = [
|
const mediaInfoTokens = [
|
||||||
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
|
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
|
||||||
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true },
|
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNotes: '1' },
|
||||||
|
|
||||||
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
|
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
|
||||||
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
|
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
|
||||||
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true },
|
{
|
||||||
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true },
|
token: '{MediaInfo AudioLanguages}',
|
||||||
|
example: '[EN+DE]',
|
||||||
|
footNotes: '1,2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: '{MediaInfo AudioLanguagesAll}',
|
||||||
|
example: '[EN]',
|
||||||
|
footNotes: '1',
|
||||||
|
},
|
||||||
|
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNotes: '1' },
|
||||||
|
|
||||||
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
|
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
|
||||||
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
|
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
|
||||||
@@ -146,11 +154,11 @@ const mediaInfoTokens = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const releaseGroupTokens = [
|
const releaseGroupTokens = [
|
||||||
{ token: '{Release Group}', example: 'Rls Grp', footNote: true },
|
{ token: '{Release Group}', example: 'Rls Grp', footNotes: '1' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const editionTokens = [
|
const editionTokens = [
|
||||||
{ token: '{Edition Tags}', example: 'IMAX', footNote: true },
|
{ token: '{Edition Tags}', example: 'IMAX', footNotes: '1' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const customFormatTokens = [
|
const customFormatTokens = [
|
||||||
@@ -287,13 +295,13 @@ function NamingModal(props: NamingModalProps) {
|
|||||||
|
|
||||||
<FieldSet legend={translate('Movie')}>
|
<FieldSet legend={translate('Movie')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{movieTokens.map(({ token, example, footNote }) => {
|
{movieTokens.map(({ token, example, footNotes }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
footNote={footNote}
|
footNotes={footNotes}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={handleOptionPress}
|
onPress={handleOptionPress}
|
||||||
@@ -303,7 +311,7 @@ function NamingModal(props: NamingModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
<div className={styles.footNote}>
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
<sup className={styles.identifier}>1</sup>
|
||||||
<InlineMarkdown data={translate('MovieFootNote')} />
|
<InlineMarkdown data={translate('MovieFootNote')} />
|
||||||
</div>
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
@@ -346,13 +354,13 @@ function NamingModal(props: NamingModalProps) {
|
|||||||
|
|
||||||
<FieldSet legend={translate('MediaInfo')}>
|
<FieldSet legend={translate('MediaInfo')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{mediaInfoTokens.map(({ token, example, footNote }) => {
|
{mediaInfoTokens.map(({ token, example, footNotes }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
footNote={footNote}
|
footNotes={footNotes}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={handleOptionPress}
|
onPress={handleOptionPress}
|
||||||
@@ -362,20 +370,25 @@ function NamingModal(props: NamingModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
<div className={styles.footNote}>
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
<sup className={styles.identifier}>1</sup>
|
||||||
<InlineMarkdown data={translate('MediaInfoFootNote')} />
|
<InlineMarkdown data={translate('MediaInfoFootNote')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<sup className={styles.identifier}>2</sup>
|
||||||
|
<InlineMarkdown data={translate('MediaInfoFootNote2')} />
|
||||||
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('ReleaseGroup')}>
|
<FieldSet legend={translate('ReleaseGroup')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{releaseGroupTokens.map(({ token, example, footNote }) => {
|
{releaseGroupTokens.map(({ token, example, footNotes }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
footNote={footNote}
|
footNotes={footNotes}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={handleOptionPress}
|
onPress={handleOptionPress}
|
||||||
@@ -385,20 +398,20 @@ function NamingModal(props: NamingModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
<div className={styles.footNote}>
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
<sup className={styles.identifier}>1</sup>
|
||||||
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
||||||
</div>
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Edition')}>
|
<FieldSet legend={translate('Edition')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{editionTokens.map(({ token, example, footNote }) => {
|
{editionTokens.map(({ token, example, footNotes }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
footNote={footNote}
|
footNotes={footNotes}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={handleOptionPress}
|
onPress={handleOptionPress}
|
||||||
@@ -408,7 +421,7 @@ function NamingModal(props: NamingModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
<div className={styles.footNote}>
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
<sup className={styles.identifier}>1</sup>
|
||||||
<InlineMarkdown data={translate('EditionFootNote')} />
|
<InlineMarkdown data={translate('EditionFootNote')} />
|
||||||
</div>
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
padding: 6px;
|
padding: 6px;
|
||||||
background-color: var(--popoverBodyBackgroundColor);
|
background-color: var(--popoverBodyBackgroundColor);
|
||||||
|
|
||||||
.footNote {
|
.footNotes {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'example': string;
|
'example': string;
|
||||||
'footNote': string;
|
'footNotes': string;
|
||||||
'isFullFilename': string;
|
'isFullFilename': string;
|
||||||
'large': string;
|
'large': string;
|
||||||
'lower': string;
|
'lower': string;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import { Size } from 'Helpers/Props/sizes';
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
import TokenCase from './TokenCase';
|
import TokenCase from './TokenCase';
|
||||||
import TokenSeparator from './TokenSeparator';
|
import TokenSeparator from './TokenSeparator';
|
||||||
@@ -14,7 +12,7 @@ interface NamingOptionProps {
|
|||||||
example: string;
|
example: string;
|
||||||
tokenCase: TokenCase;
|
tokenCase: TokenCase;
|
||||||
isFullFilename?: boolean;
|
isFullFilename?: boolean;
|
||||||
footNote?: boolean;
|
footNotes?: string;
|
||||||
size?: Extract<Size, keyof typeof styles>;
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
onPress: ({
|
onPress: ({
|
||||||
isFullFilename,
|
isFullFilename,
|
||||||
@@ -32,7 +30,7 @@ function NamingOption(props: NamingOptionProps) {
|
|||||||
example,
|
example,
|
||||||
tokenCase,
|
tokenCase,
|
||||||
isFullFilename = false,
|
isFullFilename = false,
|
||||||
footNote = false,
|
footNotes,
|
||||||
size = 'small',
|
size = 'small',
|
||||||
onPress,
|
onPress,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -66,8 +64,10 @@ function NamingOption(props: NamingOptionProps) {
|
|||||||
<div className={styles.example}>
|
<div className={styles.example}>
|
||||||
{example.replace(/ /g, tokenSeparator)}
|
{example.replace(/ /g, tokenSeparator)}
|
||||||
|
|
||||||
{footNote ? (
|
{footNotes ? (
|
||||||
<Icon className={styles.footNote} name={icons.FOOTNOTE} />
|
<div className={styles.footNotes}>
|
||||||
|
<sup>{footNotes}</sup>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import { sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
@@ -7,7 +8,8 @@ import EditMetadataModalContent, {
|
|||||||
EditMetadataModalContentProps,
|
EditMetadataModalContentProps,
|
||||||
} from './EditMetadataModalContent';
|
} from './EditMetadataModalContent';
|
||||||
|
|
||||||
interface EditMetadataModalProps extends EditMetadataModalContentProps {
|
interface EditMetadataModalProps
|
||||||
|
extends Omit<EditMetadataModalContentProps, 'advancedSettings'> {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +20,10 @@ function EditMetadataModal({
|
|||||||
}: EditMetadataModalProps) {
|
}: EditMetadataModalProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const advancedSettings = useSelector(
|
||||||
|
(state: AppState) => state.settings.advancedSettings
|
||||||
|
);
|
||||||
|
|
||||||
const handleModalClose = useCallback(() => {
|
const handleModalClose = useCallback(() => {
|
||||||
dispatch(clearPendingChanges({ section: 'metadata' }));
|
dispatch(clearPendingChanges({ section: 'metadata' }));
|
||||||
onModalClose();
|
onModalClose();
|
||||||
@@ -27,6 +33,7 @@ function EditMetadataModal({
|
|||||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
<EditMetadataModalContent
|
<EditMetadataModalContent
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
onModalClose={handleModalClose}
|
onModalClose={handleModalClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
@@ -13,6 +13,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import {
|
import {
|
||||||
saveMetadata,
|
saveMetadata,
|
||||||
@@ -41,6 +42,8 @@ function EditMetadataModalContent({
|
|||||||
(state: AppState) => state.settings.metadata
|
(state: AppState) => state.settings.metadata
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const wasSaving = usePrevious(isSaving);
|
||||||
|
|
||||||
const { settings, ...otherSettings } = useMemo(() => {
|
const { settings, ...otherSettings } = useMemo(() => {
|
||||||
const item = items.find((item) => item.id === id)!;
|
const item = items.find((item) => item.id === id)!;
|
||||||
|
|
||||||
@@ -69,6 +72,12 @@ function EditMetadataModalContent({
|
|||||||
dispatch(saveMetadata({ id }));
|
dispatch(saveMetadata({ id }));
|
||||||
}, [id, dispatch]);
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasSaving && !isSaving && !saveError) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ function Metadata({ id, name, enable, fields }: MetadataProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<EditMetadataModal
|
<EditMetadataModal
|
||||||
advancedSettings={false}
|
|
||||||
id={id}
|
id={id}
|
||||||
isOpen={isEditMetadataModalOpen}
|
isOpen={isEditMetadataModalOpen}
|
||||||
onModalClose={handleModalClose}
|
onModalClose={handleModalClose}
|
||||||
|
|||||||
+2
-2
@@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
|
|||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const actionHandlers = handleThunks({
|
|||||||
section,
|
section,
|
||||||
...item,
|
...item,
|
||||||
term,
|
term,
|
||||||
queued: true,
|
isQueued: true,
|
||||||
items: []
|
items: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -151,6 +151,8 @@ export const actionHandlers = handleThunks({
|
|||||||
abortCurrentLookup = abortRequest;
|
abortCurrentLookup = abortRequest;
|
||||||
|
|
||||||
request.done((data) => {
|
request.done((data) => {
|
||||||
|
const selectedMovie = queued.selectedMovie || data[0];
|
||||||
|
|
||||||
dispatch(updateItem({
|
dispatch(updateItem({
|
||||||
section,
|
section,
|
||||||
id: queued.id,
|
id: queued.id,
|
||||||
@@ -158,8 +160,8 @@ export const actionHandlers = handleThunks({
|
|||||||
isPopulated: true,
|
isPopulated: true,
|
||||||
error: null,
|
error: null,
|
||||||
items: data,
|
items: data,
|
||||||
queued: false,
|
isQueued: false,
|
||||||
selectedMovie: queued.selectedMovie || data[0],
|
selectedMovie,
|
||||||
updateOnly: true
|
updateOnly: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -171,7 +173,7 @@ export const actionHandlers = handleThunks({
|
|||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: xhr,
|
error: xhr,
|
||||||
queued: false,
|
isQueued: false,
|
||||||
updateOnly: true
|
updateOnly: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -278,7 +280,23 @@ export const actionHandlers = handleThunks({
|
|||||||
export const reducers = createHandleActions({
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
[CANCEL_LOOKUP_MOVIE]: function(state) {
|
[CANCEL_LOOKUP_MOVIE]: function(state) {
|
||||||
return Object.assign({}, state, { isLookingUpMovie: false });
|
queue.splice(0, queue.length);
|
||||||
|
|
||||||
|
const items = state.items.map((item) => {
|
||||||
|
if (item.isQueued) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
isQueued: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
isLookingUpMovie: false,
|
||||||
|
items
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[CLEAR_IMPORT_MOVIE]: function(state) {
|
[CLEAR_IMPORT_MOVIE]: function(state) {
|
||||||
|
|||||||
@@ -210,6 +210,12 @@ export const defaultState = {
|
|||||||
name: 'rejectionCount',
|
name: 'rejectionCount',
|
||||||
label: () => translate('RejectionCount'),
|
label: () => translate('RejectionCount'),
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'movieRequested',
|
||||||
|
label: () => translate('MovieRequested'),
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
selectedFilterKey: 'all'
|
selectedFilterKey: 'all'
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { createMovieSelectorForHook } from './createMovieSelector';
|
import { createMovieSelectorForHook } from './createMovieSelector';
|
||||||
|
|
||||||
function createMovieQualityProfileSelector(movieId: number) {
|
function createMovieQualityProfileSelector(movieId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings.qualityProfiles.items,
|
(state: AppState) => state.settings.qualityProfiles.items,
|
||||||
createMovieSelectorForHook(movieId),
|
createMovieSelectorForHook(movieId),
|
||||||
(qualityProfiles, movie = {} as Movie) => {
|
(qualityProfiles: QualityProfile[], movie = {} as Movie) => {
|
||||||
return qualityProfiles.find(
|
return qualityProfiles.find(
|
||||||
(profile) => profile.id === movie.qualityProfileId
|
(profile) => profile.id === movie.qualityProfileId
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { filesize } from 'filesize';
|
||||||
|
|
||||||
|
function formatBitrate(input: string | number) {
|
||||||
|
const size = Number(input);
|
||||||
|
|
||||||
|
if (isNaN(size)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, symbol } = filesize(size, {
|
||||||
|
base: 10,
|
||||||
|
round: 1,
|
||||||
|
output: 'object',
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${value} ${symbol}/s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formatBitrate;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type Entries<T> = {
|
||||||
|
[K in keyof T]: [K, T[K]];
|
||||||
|
}[keyof T][];
|
||||||
|
|
||||||
|
function getEntries<T extends object>(obj: T): Entries<T> {
|
||||||
|
return Object.entries(obj) as Entries<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getEntries;
|
||||||
@@ -35,7 +35,7 @@ export default function getLanguageName(code: string) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return languageNames.of(code) ?? code;
|
return languageNames.of(code) ?? code;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise<boolean> {
|
|||||||
translations = data.Strings;
|
translations = data.Strings;
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} catch (error) {
|
} catch {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import movieEntities from 'Movie/movieEntities';
|
|||||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||||
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
import MovieFileLanguageConnector from 'MovieFile/MovieFileLanguageConnector';
|
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
|
||||||
import styles from './CutoffUnmetRow.css';
|
import styles from './CutoffUnmetRow.css';
|
||||||
|
|
||||||
function CutoffUnmetRow(props) {
|
function CutoffUnmetRow(props) {
|
||||||
@@ -104,7 +104,7 @@ function CutoffUnmetRow(props) {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.languages}
|
className={styles.languages}
|
||||||
>
|
>
|
||||||
<MovieFileLanguageConnector
|
<MovieFileLanguages
|
||||||
movieFileId={movieFileId}
|
movieFileId={movieFileId}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ const error = console.error;
|
|||||||
function logError(...parameters: any[]) {
|
function logError(...parameters: any[]) {
|
||||||
const filter = parameters.find((parameter) => {
|
const filter = parameters.find((parameter) => {
|
||||||
return (
|
return (
|
||||||
parameter.includes(
|
typeof parameter === 'string' &&
|
||||||
|
(parameter.includes(
|
||||||
'Support for defaultProps will be removed from function components in a future major release'
|
'Support for defaultProps will be removed from function components in a future major release'
|
||||||
) ||
|
) ||
|
||||||
parameter.includes(
|
parameter.includes(
|
||||||
'findDOMNode is deprecated and will be removed in the next major release'
|
'findDOMNode is deprecated and will be removed in the next major release'
|
||||||
)
|
))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -22,11 +22,11 @@
|
|||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "6.6.0",
|
"@fortawesome/fontawesome-free": "6.7.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.6.0",
|
"@fortawesome/free-brands-svg-icons": "6.7.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
"@fortawesome/free-regular-svg-icons": "6.7.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||||
"@fortawesome/react-fontawesome": "0.2.2",
|
"@fortawesome/react-fontawesome": "0.2.2",
|
||||||
"@juggle/resize-observer": "3.4.0",
|
"@juggle/resize-observer": "3.4.0",
|
||||||
"@microsoft/signalr": "6.0.25",
|
"@microsoft/signalr": "6.0.25",
|
||||||
@@ -84,16 +84,16 @@
|
|||||||
"reselect": "4.1.8",
|
"reselect": "4.1.8",
|
||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"swiper": "8.3.2",
|
"swiper": "8.3.2",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.25.8",
|
"@babel/core": "7.26.0",
|
||||||
"@babel/eslint-parser": "7.25.8",
|
"@babel/eslint-parser": "7.25.9",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.25.8",
|
"@babel/plugin-proposal-export-default-from": "7.25.9",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.25.8",
|
"@babel/preset-env": "7.26.0",
|
||||||
"@babel/preset-react": "7.25.7",
|
"@babel/preset-react": "7.26.3",
|
||||||
"@babel/preset-typescript": "7.25.7",
|
"@babel/preset-typescript": "7.26.0",
|
||||||
"@types/lodash": "4.14.195",
|
"@types/lodash": "4.14.195",
|
||||||
"@types/react-document-title": "2.0.10",
|
"@types/react-document-title": "2.0.10",
|
||||||
"@types/react-lazyload": "3.2.3",
|
"@types/react-lazyload": "3.2.3",
|
||||||
@@ -102,13 +102,13 @@
|
|||||||
"@types/react-window": "1.8.8",
|
"@types/react-window": "1.8.8",
|
||||||
"@types/redux-actions": "2.6.5",
|
"@types/redux-actions": "2.6.5",
|
||||||
"@types/webpack-livereload-plugin": "2.3.6",
|
"@types/webpack-livereload-plugin": "2.3.6",
|
||||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
"@typescript-eslint/eslint-plugin": "8.18.1",
|
||||||
"@typescript-eslint/parser": "6.21.0",
|
"@typescript-eslint/parser": "8.18.1",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "10.4.20",
|
||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.38.1",
|
"core-js": "3.39.0",
|
||||||
"css-loader": "6.7.3",
|
"css-loader": "6.7.3",
|
||||||
"css-modules-typescript-loader": "4.0.1",
|
"css-modules-typescript-loader": "4.0.1",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
|
|||||||
@@ -42,17 +42,18 @@ namespace NzbDrone.Common
|
|||||||
|
|
||||||
public void CreateZip(string path, IEnumerable<string> files)
|
public void CreateZip(string path, IEnumerable<string> files)
|
||||||
{
|
{
|
||||||
using (var zipFile = ZipFile.Create(path))
|
_logger.Debug("Creating archive {0}", path);
|
||||||
|
|
||||||
|
using var zipFile = ZipFile.Create(path);
|
||||||
|
|
||||||
|
zipFile.BeginUpdate();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
zipFile.BeginUpdate();
|
zipFile.Add(file, Path.GetFileName(file));
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
zipFile.Add(file, Path.GetFileName(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
zipFile.CommitUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zipFile.CommitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExtractZip(string compressedFile, string destination)
|
private void ExtractZip(string compressedFile, string destination)
|
||||||
|
|||||||
@@ -341,10 +341,11 @@ namespace NzbDrone.Common.Disk
|
|||||||
|
|
||||||
var isCifs = targetDriveFormat == "cifs";
|
var isCifs = targetDriveFormat == "cifs";
|
||||||
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
||||||
|
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
|
||||||
|
|
||||||
if (mode.HasFlag(TransferMode.Copy))
|
if (mode.HasFlag(TransferMode.Copy))
|
||||||
{
|
{
|
||||||
if (isBtrfs)
|
if (isBtrfs || isZfs)
|
||||||
{
|
{
|
||||||
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
||||||
{
|
{
|
||||||
@@ -358,7 +359,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
|
|
||||||
if (mode.HasFlag(TransferMode.Move))
|
if (mode.HasFlag(TransferMode.Move))
|
||||||
{
|
{
|
||||||
if (isBtrfs)
|
if (isBtrfs || isZfs)
|
||||||
{
|
{
|
||||||
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,37 +17,6 @@ namespace NzbDrone.Common.Disk
|
|||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly IRuntimeInfo _runtimeInfo;
|
private readonly IRuntimeInfo _runtimeInfo;
|
||||||
|
|
||||||
private readonly HashSet<string> _setToRemove = new HashSet<string>
|
|
||||||
{
|
|
||||||
// Windows
|
|
||||||
"boot",
|
|
||||||
"bootmgr",
|
|
||||||
"cache",
|
|
||||||
"msocache",
|
|
||||||
"recovery",
|
|
||||||
"$recycle.bin",
|
|
||||||
"recycler",
|
|
||||||
"system volume information",
|
|
||||||
"temporary internet files",
|
|
||||||
"windows",
|
|
||||||
|
|
||||||
// OS X
|
|
||||||
".fseventd",
|
|
||||||
".spotlight",
|
|
||||||
".trashes",
|
|
||||||
".vol",
|
|
||||||
"cachedmessages",
|
|
||||||
"caches",
|
|
||||||
"trash",
|
|
||||||
|
|
||||||
// QNAP
|
|
||||||
".@__thumb",
|
|
||||||
|
|
||||||
// Synology
|
|
||||||
"@eadir",
|
|
||||||
"#recycle"
|
|
||||||
};
|
|
||||||
|
|
||||||
public FileSystemLookupService(IDiskProvider diskProvider, IRuntimeInfo runtimeInfo)
|
public FileSystemLookupService(IDiskProvider diskProvider, IRuntimeInfo runtimeInfo)
|
||||||
{
|
{
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
@@ -158,7 +127,7 @@ namespace NzbDrone.Common.Disk
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
directories.RemoveAll(d => _setToRemove.Contains(d.Name.ToLowerInvariant()));
|
directories.RemoveAll(d => SpecialFolders.IsSpecialFolder(d.Name));
|
||||||
|
|
||||||
return directories;
|
return directories;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Disk;
|
||||||
|
|
||||||
|
public static class SpecialFolders
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> _specialFolders = new HashSet<string>
|
||||||
|
{
|
||||||
|
// Windows
|
||||||
|
"boot",
|
||||||
|
"bootmgr",
|
||||||
|
"cache",
|
||||||
|
"msocache",
|
||||||
|
"recovery",
|
||||||
|
"$recycle.bin",
|
||||||
|
"recycler",
|
||||||
|
"system volume information",
|
||||||
|
"temporary internet files",
|
||||||
|
"windows",
|
||||||
|
|
||||||
|
// OS X
|
||||||
|
".fseventd",
|
||||||
|
".spotlight",
|
||||||
|
".trashes",
|
||||||
|
".vol",
|
||||||
|
"cachedmessages",
|
||||||
|
"caches",
|
||||||
|
"trash",
|
||||||
|
|
||||||
|
// QNAP
|
||||||
|
".@__thumb",
|
||||||
|
|
||||||
|
// Synology
|
||||||
|
"@eadir",
|
||||||
|
"#recycle"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsSpecialFolder(string folder)
|
||||||
|
{
|
||||||
|
if (folder == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _specialFolders.Contains(folder.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,10 +54,8 @@ namespace NzbDrone.Common.Extensions
|
|||||||
foreach (var item in src)
|
foreach (var item in src)
|
||||||
{
|
{
|
||||||
var key = keySelector(item);
|
var key = keySelector(item);
|
||||||
if (!result.ContainsKey(key))
|
|
||||||
{
|
result.TryAdd(key, item);
|
||||||
result[key] = item;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -69,10 +67,9 @@ namespace NzbDrone.Common.Extensions
|
|||||||
foreach (var item in src)
|
foreach (var item in src)
|
||||||
{
|
{
|
||||||
var key = keySelector(item);
|
var key = keySelector(item);
|
||||||
if (!result.ContainsKey(key))
|
var value = valueSelector(item);
|
||||||
{
|
|
||||||
result[key] = valueSelector(item);
|
result.TryAdd(key, value);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Text;
|
||||||
|
using NLog;
|
||||||
|
using NLog.Layouts.ClefJsonLayout;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Instrumentation;
|
||||||
|
|
||||||
|
public class CleansingClefLogLayout : CompactJsonLayout
|
||||||
|
{
|
||||||
|
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||||
|
{
|
||||||
|
base.RenderFormattedMessage(logEvent, target);
|
||||||
|
|
||||||
|
if (RuntimeInfo.IsProduction)
|
||||||
|
{
|
||||||
|
var result = CleanseLogMessage.Cleanse(target.ToString());
|
||||||
|
target.Clear();
|
||||||
|
target.Append(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Text;
|
||||||
|
using NLog;
|
||||||
|
using NLog.Layouts;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Instrumentation;
|
||||||
|
|
||||||
|
public class CleansingConsoleLogLayout : SimpleLayout
|
||||||
|
{
|
||||||
|
public CleansingConsoleLogLayout(string format)
|
||||||
|
: base(format)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||||
|
{
|
||||||
|
base.RenderFormattedMessage(logEvent, target);
|
||||||
|
|
||||||
|
if (RuntimeInfo.IsProduction)
|
||||||
|
{
|
||||||
|
var result = CleanseLogMessage.Cleanse(target.ToString());
|
||||||
|
target.Clear();
|
||||||
|
target.Append(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ using NLog.Targets;
|
|||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation
|
namespace NzbDrone.Common.Instrumentation
|
||||||
{
|
{
|
||||||
public class NzbDroneFileTarget : FileTarget
|
public class CleansingFileTarget : FileTarget
|
||||||
{
|
{
|
||||||
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||||
{
|
{
|
||||||
@@ -3,7 +3,6 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Config;
|
using NLog.Config;
|
||||||
using NLog.Layouts.ClefJsonLayout;
|
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
@@ -13,9 +12,11 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
{
|
{
|
||||||
public static class NzbDroneLogger
|
public static class NzbDroneLogger
|
||||||
{
|
{
|
||||||
private const string FILE_LOG_LAYOUT = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
|
private const string FileLogLayout = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
|
||||||
public const string ConsoleLogLayout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
|
private const string ConsoleFormat = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
|
||||||
public static CompactJsonLayout ClefLogLayout = new CompactJsonLayout();
|
|
||||||
|
private static readonly CleansingConsoleLogLayout CleansingConsoleLayout = new (ConsoleFormat);
|
||||||
|
private static readonly CleansingClefLogLayout ClefLogLayout = new ();
|
||||||
|
|
||||||
private static bool _isConfigured;
|
private static bool _isConfigured;
|
||||||
|
|
||||||
@@ -119,11 +120,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
? formatEnumValue
|
? formatEnumValue
|
||||||
: ConsoleLogFormat.Standard;
|
: ConsoleLogFormat.Standard;
|
||||||
|
|
||||||
coloredConsoleTarget.Layout = logFormat switch
|
ConfigureConsoleLayout(coloredConsoleTarget, logFormat);
|
||||||
{
|
|
||||||
ConsoleLogFormat.Clef => ClefLogLayout,
|
|
||||||
_ => ConsoleLogLayout
|
|
||||||
};
|
|
||||||
|
|
||||||
var loggingRule = new LoggingRule("*", level, coloredConsoleTarget);
|
var loggingRule = new LoggingRule("*", level, coloredConsoleTarget);
|
||||||
|
|
||||||
@@ -140,7 +137,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
|
|
||||||
private static void RegisterAppFile(IAppFolderInfo appFolderInfo, string name, string fileName, int maxArchiveFiles, LogLevel minLogLevel)
|
private static void RegisterAppFile(IAppFolderInfo appFolderInfo, string name, string fileName, int maxArchiveFiles, LogLevel minLogLevel)
|
||||||
{
|
{
|
||||||
var fileTarget = new NzbDroneFileTarget();
|
var fileTarget = new CleansingFileTarget();
|
||||||
|
|
||||||
fileTarget.Name = name;
|
fileTarget.Name = name;
|
||||||
fileTarget.FileName = Path.Combine(appFolderInfo.GetLogFolder(), fileName);
|
fileTarget.FileName = Path.Combine(appFolderInfo.GetLogFolder(), fileName);
|
||||||
@@ -153,7 +150,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
fileTarget.MaxArchiveFiles = maxArchiveFiles;
|
fileTarget.MaxArchiveFiles = maxArchiveFiles;
|
||||||
fileTarget.EnableFileDelete = true;
|
fileTarget.EnableFileDelete = true;
|
||||||
fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling;
|
fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling;
|
||||||
fileTarget.Layout = FILE_LOG_LAYOUT;
|
fileTarget.Layout = FileLogLayout;
|
||||||
|
|
||||||
var loggingRule = new LoggingRule("*", minLogLevel, fileTarget);
|
var loggingRule = new LoggingRule("*", minLogLevel, fileTarget);
|
||||||
|
|
||||||
@@ -172,7 +169,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
fileTarget.ConcurrentWrites = false;
|
fileTarget.ConcurrentWrites = false;
|
||||||
fileTarget.ConcurrentWriteAttemptDelay = 50;
|
fileTarget.ConcurrentWriteAttemptDelay = 50;
|
||||||
fileTarget.ConcurrentWriteAttempts = 100;
|
fileTarget.ConcurrentWriteAttempts = 100;
|
||||||
fileTarget.Layout = FILE_LOG_LAYOUT;
|
fileTarget.Layout = FileLogLayout;
|
||||||
|
|
||||||
var loggingRule = new LoggingRule("*", LogLevel.Trace, fileTarget);
|
var loggingRule = new LoggingRule("*", LogLevel.Trace, fileTarget);
|
||||||
|
|
||||||
@@ -217,6 +214,15 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
{
|
{
|
||||||
return GetLogger(obj.GetType());
|
return GetLogger(obj.GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ConfigureConsoleLayout(ColoredConsoleTarget target, ConsoleLogFormat format)
|
||||||
|
{
|
||||||
|
target.Layout = format switch
|
||||||
|
{
|
||||||
|
ConsoleLogFormat.Clef => NzbDroneLogger.ClefLogLayout,
|
||||||
|
_ => NzbDroneLogger.CleansingConsoleLayout
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ConsoleLogFormat
|
public enum ConsoleLogFormat
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.Environment = BuildInfo.Branch;
|
o.Environment = BuildInfo.Branch;
|
||||||
|
|
||||||
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
||||||
o.AutoSessionTracking = true;
|
o.AutoSessionTracking = false;
|
||||||
|
|
||||||
// Caches files in the event device is offline
|
// Caches files in the event device is offline
|
||||||
// Sentry creates a 'sentry' sub directory, no need to concat here
|
// Sentry creates a 'sentry' sub directory, no need to concat here
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="5.3.4" />
|
<PackageReference Include="NLog" Version="5.3.4" />
|
||||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.2" />
|
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.3" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
|
||||||
<PackageReference Include="Npgsql" Version="7.0.9" />
|
<PackageReference Include="Npgsql" Version="7.0.9" />
|
||||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Datastore.Migration;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class stevenlu_update_urlFixture : MigrationTest<stevenlu_update_url>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_update_stevenlu_url()
|
||||||
|
{
|
||||||
|
var db = WithMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("ImportLists").Row(new
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnableAuto = true,
|
||||||
|
Name = "StevenLu List",
|
||||||
|
QualityProfileId = 1,
|
||||||
|
MinimumAvailability = 1,
|
||||||
|
RootFolderPath = "/movies",
|
||||||
|
Monitor = 0,
|
||||||
|
SearchOnAdd = true,
|
||||||
|
Tags = "[]",
|
||||||
|
Implementation = "StevenLuImport",
|
||||||
|
ConfigContract = "StevenLuSettings",
|
||||||
|
Settings = new StevenLuSettings241
|
||||||
|
{
|
||||||
|
Link = "https://s3.amazonaws.com/popular-movies/movies.json"
|
||||||
|
}.ToJson()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = db.Query<ImportListDefinition241>("SELECT \"Id\", \"Settings\" FROM \"ImportLists\"");
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().Settings.Link.Should().Be("https://popular-movies-data.stevenlu.com/movies.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListDefinition241 : ModelBase
|
||||||
|
{
|
||||||
|
public StevenLuSettings241 Settings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StevenLuSettings241
|
||||||
|
{
|
||||||
|
public string Link { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,32 +20,32 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
private List<ReleaseInfo> _reports;
|
private List<ReleaseInfo> _reports;
|
||||||
private RemoteMovie _remoteEpisode;
|
private RemoteMovie _remoteEpisode;
|
||||||
|
|
||||||
private Mock<IDecisionEngineSpecification> _pass1;
|
private Mock<IDownloadDecisionEngineSpecification> _pass1;
|
||||||
private Mock<IDecisionEngineSpecification> _pass2;
|
private Mock<IDownloadDecisionEngineSpecification> _pass2;
|
||||||
private Mock<IDecisionEngineSpecification> _pass3;
|
private Mock<IDownloadDecisionEngineSpecification> _pass3;
|
||||||
|
|
||||||
private Mock<IDecisionEngineSpecification> _fail1;
|
private Mock<IDownloadDecisionEngineSpecification> _fail1;
|
||||||
private Mock<IDecisionEngineSpecification> _fail2;
|
private Mock<IDownloadDecisionEngineSpecification> _fail2;
|
||||||
private Mock<IDecisionEngineSpecification> _fail3;
|
private Mock<IDownloadDecisionEngineSpecification> _fail3;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_pass1 = new Mock<IDecisionEngineSpecification>();
|
_pass1 = new Mock<IDownloadDecisionEngineSpecification>();
|
||||||
_pass2 = new Mock<IDecisionEngineSpecification>();
|
_pass2 = new Mock<IDownloadDecisionEngineSpecification>();
|
||||||
_pass3 = new Mock<IDecisionEngineSpecification>();
|
_pass3 = new Mock<IDownloadDecisionEngineSpecification>();
|
||||||
|
|
||||||
_fail1 = new Mock<IDecisionEngineSpecification>();
|
_fail1 = new Mock<IDownloadDecisionEngineSpecification>();
|
||||||
_fail2 = new Mock<IDecisionEngineSpecification>();
|
_fail2 = new Mock<IDownloadDecisionEngineSpecification>();
|
||||||
_fail3 = new Mock<IDecisionEngineSpecification>();
|
_fail3 = new Mock<IDownloadDecisionEngineSpecification>();
|
||||||
|
|
||||||
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Accept);
|
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Accept);
|
||||||
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Accept);
|
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Accept);
|
||||||
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Accept);
|
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Accept);
|
||||||
|
|
||||||
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Reject("fail1"));
|
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail1"));
|
||||||
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Reject("fail2"));
|
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail2"));
|
||||||
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(Decision.Reject("fail3"));
|
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteMovie>(), null)).Returns(DownloadSpecDecision.Reject(DownloadRejectionReason.Unknown, "fail3"));
|
||||||
|
|
||||||
_reports = new List<ReleaseInfo> { new ReleaseInfo { Title = "Trolls.2016.720p.WEB-DL.DD5.1.H264-FGT" } };
|
_reports = new List<ReleaseInfo> { new ReleaseInfo { Title = "Trolls.2016.720p.WEB-DL.DD5.1.H264-FGT" } };
|
||||||
_remoteEpisode = new RemoteMovie
|
_remoteEpisode = new RemoteMovie
|
||||||
@@ -58,9 +58,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
.Setup(c => c.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())).Returns(_remoteEpisode);
|
.Setup(c => c.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>())).Returns(_remoteEpisode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GivenSpecifications(params Mock<IDecisionEngineSpecification>[] mocks)
|
private void GivenSpecifications(params Mock<IDownloadDecisionEngineSpecification>[] mocks)
|
||||||
{
|
{
|
||||||
Mocker.SetConstant<IEnumerable<IDecisionEngineSpecification>>(mocks.Select(c => c.Object));
|
Mocker.SetConstant<IEnumerable<IDownloadDecisionEngineSpecification>>(mocks.Select(c => c.Object));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using FluentAssertions;
|
|||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
@@ -337,5 +338,42 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_format_cutoff_is_above_current_score_and_is_revision_upgrade()
|
||||||
|
{
|
||||||
|
var customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 };
|
||||||
|
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.SetupGet(s => s.DownloadPropersAndRepacks)
|
||||||
|
.Returns(ProperDownloadTypes.DoNotPrefer);
|
||||||
|
|
||||||
|
GivenProfile(new QualityProfile
|
||||||
|
{
|
||||||
|
Cutoff = Quality.SDTV.Id,
|
||||||
|
MinFormatScore = 0,
|
||||||
|
CutoffFormatScore = 10000,
|
||||||
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
|
FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"),
|
||||||
|
UpgradeAllowed = false
|
||||||
|
});
|
||||||
|
|
||||||
|
_parseResultSingle.Movie.QualityProfile.FormatItems = new List<ProfileFormatItem>
|
||||||
|
{
|
||||||
|
new ProfileFormatItem
|
||||||
|
{
|
||||||
|
Format = customFormat,
|
||||||
|
Score = 50
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GivenFileQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)));
|
||||||
|
GivenNewQuality(new QualityModel(Quality.WEBDL1080p, new Revision(version: 2)));
|
||||||
|
|
||||||
|
GivenOldCustomFormats(new List<CustomFormat>());
|
||||||
|
GivenNewCustomFormats(new List<CustomFormat> { customFormat });
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
.Should().Be(UpgradeableRejectReason.None);
|
.Should().Be(UpgradeableRejectReason.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_proper_and_autoDownloadPropers_is_do_not_prefer()
|
||||||
|
{
|
||||||
|
GivenAutoDownloadPropers(ProperDownloadTypes.DoNotPrefer);
|
||||||
|
|
||||||
|
var profile = new QualityProfile
|
||||||
|
{
|
||||||
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Subject.IsUpgradable(
|
||||||
|
profile,
|
||||||
|
new QualityModel(Quality.DVD, new Revision(version: 1)),
|
||||||
|
new List<CustomFormat>(),
|
||||||
|
new QualityModel(Quality.DVD, new Revision(version: 2)),
|
||||||
|
new List<CustomFormat>())
|
||||||
|
.Should().Be(UpgradeableRejectReason.UpgradesNotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_false_if_release_and_existing_file_are_the_same()
|
public void should_return_false_if_release_and_existing_file_are_the_same()
|
||||||
{
|
{
|
||||||
@@ -121,7 +140,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
new List<CustomFormat>(),
|
new List<CustomFormat>(),
|
||||||
new QualityModel(Quality.HDTV720p, new Revision(version: 1)),
|
new QualityModel(Quality.HDTV720p, new Revision(version: 1)),
|
||||||
new List<CustomFormat>())
|
new List<CustomFormat>())
|
||||||
.Should().Be(UpgradeableRejectReason.CustomFormatScore);
|
.Should().Be(UpgradeableRejectReason.UpgradesNotAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
+132
-8
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
};
|
};
|
||||||
Mocker.GetMock<IIndexerFactory>()
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
.Setup(v => v.Get(1))
|
.Setup(v => v.Find(1))
|
||||||
.Returns(indexerDefinition);
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
|
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
|
||||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
_remoteMovie.Release.Title = releaseTitle;
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
||||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
|
||||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerFactory>()
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
.Setup(v => v.Get(1))
|
.Setup(v => v.Find(1))
|
||||||
.Returns(indexerDefinition1);
|
.Returns(indexerDefinition1);
|
||||||
|
|
||||||
Mocker.GetMock<IIndexerFactory>()
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
@@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
_remoteMovie.Release.Title = releaseTitle;
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
||||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
|
||||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
};
|
};
|
||||||
Mocker.GetMock<IIndexerFactory>()
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
.Setup(v => v.Get(1))
|
.Setup(v => v.Find(1))
|
||||||
.Returns(indexerDefinition);
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown }, releaseTitle);
|
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown }, releaseTitle);
|
||||||
@@ -163,7 +163,51 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
_remoteMovie.Release.Title = releaseTitle;
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
||||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
|
||||||
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_multi_languages_when_release_as_specified_language_and_indexer_has_multi_languages_configuration()
|
||||||
|
{
|
||||||
|
var releaseTitle = "Some.Movie.2024.MULTi.VFF.VFQ.1080p.BluRay.DTS.HDMA.x264-RlsGroup";
|
||||||
|
var indexerDefinition = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
|
};
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.Find(1))
|
||||||
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
|
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.French }, releaseTitle);
|
||||||
|
_remoteMovie.Release.IndexerId = 1;
|
||||||
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
||||||
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
|
||||||
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_multi_languages_when_release_as_other_language_and_indexer_has_multi_languages_configuration()
|
||||||
|
{
|
||||||
|
var releaseTitle = "Some.Movie.2024.MULTi.GERMAN.1080p.BluRay.DTS.HDMA.x264-RlsGroup";
|
||||||
|
var indexerDefinition = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
|
};
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.Find(1))
|
||||||
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
|
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { Language.German }, releaseTitle);
|
||||||
|
_remoteMovie.Release.IndexerId = 1;
|
||||||
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French, Language.German });
|
||||||
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
|
||||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +221,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
Settings = new TorrentRssIndexerSettings { }
|
Settings = new TorrentRssIndexerSettings { }
|
||||||
};
|
};
|
||||||
Mocker.GetMock<IIndexerFactory>()
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
.Setup(v => v.Get(1))
|
.Setup(v => v.Find(1))
|
||||||
.Returns(indexerDefinition);
|
.Returns(indexerDefinition);
|
||||||
|
|
||||||
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
|
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
|
||||||
@@ -185,7 +229,7 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
_remoteMovie.Release.Title = releaseTitle;
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage });
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage });
|
||||||
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Get(1), Times.Once());
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(1), Times.Once());
|
||||||
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,5 +292,85 @@ namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators
|
|||||||
|
|
||||||
Subject.Aggregate(_remoteMovie).Languages.Should().Equal(Language.Greek);
|
Subject.Aggregate(_remoteMovie).Languages.Should().Equal(Language.Greek);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_multi_languages_from_indexer_with_name_when_indexer_id_does_not_exist()
|
||||||
|
{
|
||||||
|
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||||
|
var indexerDefinition1 = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Name = "MyIndexer1",
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
|
};
|
||||||
|
var indexerDefinition2 = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
Name = "MyIndexer2",
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } }
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.Find(1))
|
||||||
|
.Returns(null as IndexerDefinition);
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.FindByName("MyIndexer1"))
|
||||||
|
.Returns(indexerDefinition1);
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.All())
|
||||||
|
.Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 });
|
||||||
|
|
||||||
|
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
|
||||||
|
_remoteMovie.Release.IndexerId = 10;
|
||||||
|
_remoteMovie.Release.Indexer = "MyIndexer1";
|
||||||
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
||||||
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(10), Times.Once());
|
||||||
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer1"), Times.Once());
|
||||||
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_multi_languages_from_indexer_with_name_when_indexer_id_not_available()
|
||||||
|
{
|
||||||
|
var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup";
|
||||||
|
var indexerDefinition1 = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Name = "MyIndexer1",
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.French.Id } }
|
||||||
|
};
|
||||||
|
var indexerDefinition2 = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
Name = "MyIndexer2",
|
||||||
|
Settings = new TorrentRssIndexerSettings { MultiLanguages = new List<int> { Language.Original.Id, Language.German.Id } }
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.Find(1))
|
||||||
|
.Returns(null as IndexerDefinition);
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.FindByName("MyIndexer1"))
|
||||||
|
.Returns(indexerDefinition1);
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(v => v.All())
|
||||||
|
.Returns(new List<IndexerDefinition>() { indexerDefinition1, indexerDefinition2 });
|
||||||
|
|
||||||
|
_remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List<Language> { }, releaseTitle);
|
||||||
|
_remoteMovie.Release.IndexerId = 0;
|
||||||
|
_remoteMovie.Release.Indexer = "MyIndexer1";
|
||||||
|
_remoteMovie.Release.Title = releaseTitle;
|
||||||
|
|
||||||
|
Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List<Language> { _movie.MovieMetadata.Value.OriginalLanguage, Language.French });
|
||||||
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.Find(10), Times.Never());
|
||||||
|
Mocker.GetMock<IIndexerFactory>().Verify(c => c.FindByName("MyIndexer1"), Times.Once());
|
||||||
|
Mocker.GetMock<IIndexerFactory>().VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using FluentAssertions;
|
|||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
using NzbDrone.Core.History;
|
using NzbDrone.Core.History;
|
||||||
@@ -107,11 +106,11 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
{
|
{
|
||||||
new ImportResult(
|
new ImportResult(
|
||||||
new ImportDecision(
|
new ImportDecision(
|
||||||
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new Rejection("Rejected!")), "Test Failure"),
|
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure"),
|
||||||
|
|
||||||
new ImportResult(
|
new ImportResult(
|
||||||
new ImportDecision(
|
new ImportDecision(
|
||||||
new LocalMovie { Path = @"C:\TestPath\Droned.1999.mkv" }, new Rejection("Rejected!")), "Test Failure")
|
new LocalMovie { Path = @"C:\TestPath\Droned.1999.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure")
|
||||||
});
|
});
|
||||||
|
|
||||||
Subject.Import(_trackedDownload);
|
Subject.Import(_trackedDownload);
|
||||||
@@ -131,11 +130,11 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
{
|
{
|
||||||
new ImportResult(
|
new ImportResult(
|
||||||
new ImportDecision(
|
new ImportDecision(
|
||||||
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new Rejection("Rejected!")), "Test Failure"),
|
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure"),
|
||||||
|
|
||||||
new ImportResult(
|
new ImportResult(
|
||||||
new ImportDecision(
|
new ImportDecision(
|
||||||
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new Rejection("Rejected!")), "Test Failure")
|
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }, new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")), "Test Failure")
|
||||||
});
|
});
|
||||||
|
|
||||||
_trackedDownload.RemoteMovie.Movie = new Movie();
|
_trackedDownload.RemoteMovie.Movie = new Movie();
|
||||||
|
|||||||
+6
-6
@@ -189,8 +189,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
{
|
{
|
||||||
var decisions = new List<DownloadDecision>();
|
var decisions = new List<DownloadDecision>();
|
||||||
RemoteMovie remoteMovie = null;
|
RemoteMovie remoteMovie = null;
|
||||||
decisions.Add(new DownloadDecision(remoteMovie, new Rejection("Failure!")));
|
decisions.Add(new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!")));
|
||||||
decisions.Add(new DownloadDecision(remoteMovie, new Rejection("Failure!")));
|
decisions.Add(new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!")));
|
||||||
|
|
||||||
Subject.GetQualifiedReports(decisions).Should().BeEmpty();
|
Subject.GetQualifiedReports(decisions).Should().BeEmpty();
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
var remoteMovie = GetRemoteMovie(new QualityModel(Quality.HDTV720p));
|
var remoteMovie = GetRemoteMovie(new QualityModel(Quality.HDTV720p));
|
||||||
|
|
||||||
var decisions = new List<DownloadDecision>();
|
var decisions = new List<DownloadDecision>();
|
||||||
decisions.Add(new DownloadDecision(remoteMovie, new Rejection("Failure!", RejectionType.Temporary)));
|
decisions.Add(new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Never());
|
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Never());
|
||||||
@@ -214,7 +214,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
|
|
||||||
var decisions = new List<DownloadDecision>();
|
var decisions = new List<DownloadDecision>();
|
||||||
decisions.Add(new DownloadDecision(removeMovie));
|
decisions.Add(new DownloadDecision(removeMovie));
|
||||||
decisions.Add(new DownloadDecision(removeMovie, new Rejection("Failure!", RejectionType.Temporary)));
|
decisions.Add(new DownloadDecision(removeMovie, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never());
|
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never());
|
||||||
@@ -226,8 +226,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||||||
var remoteEpisode = GetRemoteMovie(new QualityModel(Quality.HDTV720p));
|
var remoteEpisode = GetRemoteMovie(new QualityModel(Quality.HDTV720p));
|
||||||
|
|
||||||
var decisions = new List<DownloadDecision>();
|
var decisions = new List<DownloadDecision>();
|
||||||
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
|
decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
|
||||||
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
|
decisions.Add(new DownloadDecision(remoteEpisode, new DownloadRejection(DownloadRejectionReason.Unknown, "Failure!", RejectionType.Temporary)));
|
||||||
|
|
||||||
await Subject.ProcessDecisions(decisions);
|
await Subject.ProcessDecisions(decisions);
|
||||||
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once());
|
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once());
|
||||||
|
|||||||
+17
@@ -95,5 +95,22 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
|
|||||||
|
|
||||||
VerifySingleItem(DownloadItemStatus.Completed);
|
VerifySingleItem(DownloadItemStatus.Completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("@eaDir")]
|
||||||
|
[TestCase(".@__thumb")]
|
||||||
|
public void GetItems_should_not_include_special_subfolders(string folderName)
|
||||||
|
{
|
||||||
|
GivenCompletedItem();
|
||||||
|
|
||||||
|
var targetDir = Path.Combine(_completedDownloadFolder, folderName);
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(c => c.GetDirectories(_completedDownloadFolder))
|
||||||
|
.Returns(new[] { targetDir });
|
||||||
|
|
||||||
|
var items = Subject.GetItems(_completedDownloadFolder, TimeSpan.FromMilliseconds(50)).ToList();
|
||||||
|
|
||||||
|
items.Count.Should().Be(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
@@ -711,6 +711,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 1.1006066990976857f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_just_under_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 0.9999f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase("pausedUP")]
|
[TestCase("pausedUP")]
|
||||||
[TestCase("stoppedUP")]
|
[TestCase("stoppedUP")]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
|
||||||
@@ -723,6 +747,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(2.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 1.1006066990976857f, ratioLimit: 1.1f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("pausedUP")]
|
||||||
|
[TestCase("stoppedUP")]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_just_under_overridden_max_ratio_reached_after_rounding_and_paused(string state)
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(2.0f);
|
||||||
|
GivenCompletedTorrent(state, ratio: 0.9999f, ratioLimit: 1.0f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase("pausedUP")]
|
[TestCase("pausedUP")]
|
||||||
[TestCase("stoppedUP")]
|
[TestCase("stoppedUP")]
|
||||||
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||||||
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
|
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
|
||||||
_remoteMovie.Release = _release;
|
_remoteMovie.Release = _release;
|
||||||
|
|
||||||
_temporarilyRejected = new DownloadDecision(_remoteMovie, new Rejection("Temp Rejected", RejectionType.Temporary));
|
_temporarilyRejected = new DownloadDecision(_remoteMovie, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary));
|
||||||
|
|
||||||
_heldReleases = new List<PendingRelease>();
|
_heldReleases = new List<PendingRelease>();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||||||
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
|
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
|
||||||
_remoteMovie.Release = _release;
|
_remoteMovie.Release = _release;
|
||||||
|
|
||||||
_temporarilyRejected = new DownloadDecision(_remoteMovie, new Rejection("Temp Rejected", RejectionType.Temporary));
|
_temporarilyRejected = new DownloadDecision(_remoteMovie, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary));
|
||||||
|
|
||||||
_heldReleases = new List<PendingRelease>();
|
_heldReleases = new List<PendingRelease>();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||||||
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
|
_remoteMovie.ParsedMovieInfo = _parsedMovieInfo;
|
||||||
_remoteMovie.Release = _release;
|
_remoteMovie.Release = _release;
|
||||||
|
|
||||||
_temporarilyRejected = new DownloadDecision(_remoteMovie, new Rejection("Temp Rejected", RejectionType.Temporary));
|
_temporarilyRejected = new DownloadDecision(_remoteMovie, new DownloadRejection(DownloadRejectionReason.MinimumAgeDelay, "Temp Rejected", RejectionType.Temporary));
|
||||||
|
|
||||||
Mocker.GetMock<IPendingReleaseRepository>()
|
Mocker.GetMock<IPendingReleaseRepository>()
|
||||||
.Setup(s => s.All())
|
.Setup(s => s.All())
|
||||||
|
|||||||
@@ -76,6 +76,19 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
Subject.Check().ShouldBeWarning(wikiFragment: "downloads-in-root-folder");
|
Subject.Check().ShouldBeWarning(wikiFragment: "downloads-in-root-folder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_warning_if_downloading_inside_root_folder()
|
||||||
|
{
|
||||||
|
var rootFolderPath = "c:\\Test".AsOsAgnostic();
|
||||||
|
var downloadRootPath = "c:\\Test\\Downloads".AsOsAgnostic();
|
||||||
|
|
||||||
|
GivenRootFolder(rootFolderPath);
|
||||||
|
|
||||||
|
_clientStatus.OutputRootFolders = new List<OsPath> { new (downloadRootPath) };
|
||||||
|
|
||||||
|
Subject.Check().ShouldBeWarning();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_ok_if_not_downloading_to_root_folder()
|
public void should_return_ok_if_not_downloading_to_root_folder()
|
||||||
{
|
{
|
||||||
@@ -87,7 +100,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource("DownloadClientExceptions")]
|
[TestCaseSource(nameof(DownloadClientExceptions))]
|
||||||
public void should_return_ok_if_client_throws_downloadclientexception(Exception ex)
|
public void should_return_ok_if_client_throws_downloadclientexception(Exception ex)
|
||||||
{
|
{
|
||||||
_downloadClient.Setup(s => s.GetStatus())
|
_downloadClient.Setup(s => s.GetStatus())
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ namespace NzbDrone.Core.Test.Languages
|
|||||||
new object[] { 48, Language.Malayalam },
|
new object[] { 48, Language.Malayalam },
|
||||||
new object[] { 49, Language.Kannada },
|
new object[] { 49, Language.Kannada },
|
||||||
new object[] { 50, Language.Albanian },
|
new object[] { 50, Language.Albanian },
|
||||||
new object[] { 51, Language.Afrikaans }
|
new object[] { 51, Language.Afrikaans },
|
||||||
|
new object[] { 52, Language.Marathi },
|
||||||
|
new object[] { 53, Language.Tagalog },
|
||||||
};
|
};
|
||||||
|
|
||||||
public static object[] ToIntCases =
|
public static object[] ToIntCases =
|
||||||
@@ -121,7 +123,9 @@ namespace NzbDrone.Core.Test.Languages
|
|||||||
new object[] { Language.Malayalam, 48 },
|
new object[] { Language.Malayalam, 48 },
|
||||||
new object[] { Language.Kannada, 49 },
|
new object[] { Language.Kannada, 49 },
|
||||||
new object[] { Language.Albanian, 50 },
|
new object[] { Language.Albanian, 50 },
|
||||||
new object[] { Language.Afrikaans, 51 }
|
new object[] { Language.Afrikaans, 51 },
|
||||||
|
new object[] { Language.Marathi, 52 },
|
||||||
|
new object[] { Language.Tagalog, 53 },
|
||||||
};
|
};
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Moq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.History;
|
using NzbDrone.Core.History;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
@@ -46,9 +45,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
|||||||
.With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic())
|
.With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new Rejection("Rejected!")));
|
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")));
|
||||||
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new Rejection("Rejected!")));
|
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")));
|
||||||
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new Rejection("Rejected!")));
|
_rejectedDecisions.Add(new ImportDecision(new LocalMovie(), new ImportRejection(ImportRejectionReason.Unknown, "Rejected!")));
|
||||||
|
|
||||||
_approvedDecisions.Add(new ImportDecision(
|
_approvedDecisions.Add(new ImportDecision(
|
||||||
new LocalMovie
|
new LocalMovie
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using FizzWare.NBuilder;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.MediaFiles.MovieImport;
|
using NzbDrone.Core.MediaFiles.MovieImport;
|
||||||
@@ -49,13 +48,13 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
|||||||
_fail2 = new Mock<IImportDecisionEngineSpecification>();
|
_fail2 = new Mock<IImportDecisionEngineSpecification>();
|
||||||
_fail3 = new Mock<IImportDecisionEngineSpecification>();
|
_fail3 = new Mock<IImportDecisionEngineSpecification>();
|
||||||
|
|
||||||
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
|
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept());
|
||||||
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
|
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept());
|
||||||
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Accept());
|
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Accept());
|
||||||
|
|
||||||
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail1"));
|
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail1"));
|
||||||
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail2"));
|
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail2"));
|
||||||
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail3"));
|
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(ImportSpecDecision.Reject(ImportRejectionReason.Unknown, "_fail3"));
|
||||||
|
|
||||||
_movie = Builder<Movie>.CreateNew()
|
_movie = Builder<Movie>.CreateNew()
|
||||||
.With(e => e.Path = @"C:\Test\Movie".AsOsAgnostic())
|
.With(e => e.Path = @"C:\Test\Movie".AsOsAgnostic())
|
||||||
|
|||||||
+11
@@ -54,6 +54,17 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Specifications
|
|||||||
@"C:\Test\Downloaded\Bad Boys (2006) part1.mkv",
|
@"C:\Test\Downloaded\Bad Boys (2006) part1.mkv",
|
||||||
@"C:\Test\Downloaded\Bad Boys (2006) part2.mkv"
|
@"C:\Test\Downloaded\Bad Boys (2006) part2.mkv"
|
||||||
})]
|
})]
|
||||||
|
|
||||||
|
[TestCase(new object[]
|
||||||
|
{
|
||||||
|
@"C:\Test\Downloaded\Bad Boys (2006) pt1.mkv",
|
||||||
|
@"C:\Test\Downloaded\Bad Boys (2006) pt2.mkv"
|
||||||
|
})]
|
||||||
|
[TestCase(new object[]
|
||||||
|
{
|
||||||
|
@"C:\Test\Downloaded\Bad Boys (2006) P1.mkv",
|
||||||
|
@"C:\Test\Downloaded\Bad Boys (2006) P2.mkv"
|
||||||
|
})]
|
||||||
[TestCase(new object[]
|
[TestCase(new object[]
|
||||||
{
|
{
|
||||||
@"C:\Test\Downloaded\blah blah - cd 1.mvk",
|
@"C:\Test\Downloaded\blah blah - cd 1.mvk",
|
||||||
|
|||||||
@@ -62,5 +62,23 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
var result = IsoLanguages.Find(isoCode);
|
var result = IsoLanguages.Find(isoCode);
|
||||||
result.Language.Should().Be(Language.Afrikaans);
|
result.Language.Should().Be(Language.Afrikaans);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("mr")]
|
||||||
|
[TestCase("mar")]
|
||||||
|
[TestCase("mr-IN")]
|
||||||
|
public void should_return_marathi(string isoCode)
|
||||||
|
{
|
||||||
|
var result = IsoLanguages.Find(isoCode);
|
||||||
|
result.Language.Should().Be(Language.Marathi);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("tl")]
|
||||||
|
[TestCase("tgl")]
|
||||||
|
[TestCase("tl-PH")]
|
||||||
|
public void should_return_tagalog(string isoCode)
|
||||||
|
{
|
||||||
|
var result = IsoLanguages.Find(isoCode);
|
||||||
|
result.Language.Should().Be(Language.Tagalog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Movie Title : Other Title 2010 x264.720p.Blu-ray Rip HD.VOSTFR.VFF. ONLY")]
|
[TestCase("Movie Title : Other Title 2010 x264.720p.Blu-ray Rip HD.VOSTFR.VFF. ONLY")]
|
||||||
[TestCase("Movie Title 2019 HEVC.2160p.Blu-ray 4K.VOSTFR.VFF. JATO")]
|
[TestCase("Movie Title 2019 HEVC.2160p.Blu-ray 4K.VOSTFR.VFF. JATO")]
|
||||||
[TestCase("Movie.Title.1956.MULTi.VF.Bluray.1080p.REMUX.AC3.x264")]
|
[TestCase("Movie.Title.1956.MULTi.VF.Bluray.1080p.REMUX.AC3.x264")]
|
||||||
|
[TestCase("Movie.Title.2016.ENG-ITA-FRE.AAC.1080p.WebDL.x264")]
|
||||||
|
[TestCase("Movie Title 2016 (BDrip 1080p ENG-ITA-FRE) Multisub x264")]
|
||||||
public void should_parse_language_french(string postTitle)
|
public void should_parse_language_french(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -87,7 +89,13 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Movie.Title.1994.German.1080p.XviD-LOL")]
|
[TestCase("Movie.Title.1994.German.1080p.XviD-LOL")]
|
||||||
|
[TestCase("Movie.Title.2016.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP")]
|
||||||
|
[TestCase("Movie Title 2016 - Kampfhaehne - mkv - by Videomann")]
|
||||||
[TestCase("Movie.Title.2016.Ger.Dub.AAC.1080p.WebDL.x264-TKP21")]
|
[TestCase("Movie.Title.2016.Ger.Dub.AAC.1080p.WebDL.x264-TKP21")]
|
||||||
|
[TestCase("Movie.Title.2016.Ger.AAC.1080p.WebDL.x264-TKP21")]
|
||||||
|
[TestCase("Movie.Title.2016.Hun/Ger/Ita.AAC.1080p.WebDL.x264-TKP21")]
|
||||||
|
[TestCase("Movie.Title.2016.1080p.10Bit.HEVC.WEBRip.HIN-ENG-GER.DD5.1.H.265")]
|
||||||
|
[TestCase("Movie.Title.2016.HU-IT-DE.AAC.1080p.WebDL.x264")]
|
||||||
public void should_parse_language_german(string postTitle)
|
public void should_parse_language_german(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -96,6 +104,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Movie.Title.1994.Italian.1080p.XviD-LOL")]
|
[TestCase("Movie.Title.1994.Italian.1080p.XviD-LOL")]
|
||||||
|
[TestCase("Movie.Title.2016.ENG-FRE-ITA.AAC.1080p.WebDL.x264")]
|
||||||
|
[TestCase("Movie Title 2016 (BDrip 1080p ENG-FRE-ITA) Multisub x264")]
|
||||||
public void should_parse_language_italian(string postTitle)
|
public void should_parse_language_italian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -120,6 +130,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Movie.Title.1994.Japanese.1080p.XviD-LOL")]
|
[TestCase("Movie.Title.1994.Japanese.1080p.XviD-LOL")]
|
||||||
|
[TestCase("Movie.Title (1988) 2160p HDR 5.1 Eng - Jpn x265 10bit")]
|
||||||
|
[TestCase("Movie Title (1985) (1080p.AC3 ITA-ENG-JPN)")]
|
||||||
public void should_parse_language_japanese(string postTitle)
|
public void should_parse_language_japanese(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -291,6 +303,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Movie.Title.1994.Korean.1080p.XviD-LOL")]
|
[TestCase("Movie.Title.1994.Korean.1080p.XviD-LOL")]
|
||||||
|
[TestCase("Movie Title [2006] BDRip 720p [Kor Rus] GROUP")]
|
||||||
|
[TestCase("Movie.Title.2019.KOR.1080p.HDRip.H264.AAC-GROUP")]
|
||||||
public void should_parse_language_korean(string postTitle)
|
public void should_parse_language_korean(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -460,6 +474,22 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
result.Should().Contain(Language.Afrikaans);
|
result.Should().Contain(Language.Afrikaans);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("Movie Title 2015 Marathi 1080p WebRip x264 AC3 5.1 ESubs [TMB]")]
|
||||||
|
[TestCase("Movie.Title.(2018).720p.CensorRip.Marathi.x264.AAC.-.LHDm@Telly")]
|
||||||
|
public void should_parse_language_marathi(string postTitle)
|
||||||
|
{
|
||||||
|
var result = LanguageParser.ParseLanguages(postTitle);
|
||||||
|
result.Should().Contain(Language.Marathi);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("Movie Title 2024 1080p Tagalog WEB-DL HEVC x265 BONE")]
|
||||||
|
[TestCase("Movie.Title.2022.720p.Tagalog.WEB-DL.AAC.x264-Mkvking")]
|
||||||
|
public void should_parse_language_tagalog(string postTitle)
|
||||||
|
{
|
||||||
|
var result = LanguageParser.ParseLanguages(postTitle);
|
||||||
|
result.Should().Contain(Language.Tagalog);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase("Movie.Title.en.sub")]
|
[TestCase("Movie.Title.en.sub")]
|
||||||
[TestCase("Movie Title.eng.sub")]
|
[TestCase("Movie Title.eng.sub")]
|
||||||
[TestCase("Movie.Title.eng.forced.sub")]
|
[TestCase("Movie.Title.eng.forced.sub")]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
|||||||
public override int Order => 1;
|
public override int Order => 1;
|
||||||
public override string ImplementationName => "Genre";
|
public override string ImplementationName => "Genre";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Genre(s)", Type = FieldType.Tag)]
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationGenre", Type = FieldType.Tag)]
|
||||||
public IEnumerable<string> Value { get; set; }
|
public IEnumerable<string> Value { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
|||||||
public override int Order => 1;
|
public override int Order => 1;
|
||||||
public override string ImplementationName => "Original Language";
|
public override string ImplementationName => "Original Language";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))]
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationOriginalLanguage", Type = FieldType.Select, SelectOptions = typeof(OriginalLanguageFieldConverter))]
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
|||||||
public override int Order => 1;
|
public override int Order => 1;
|
||||||
public override string ImplementationName => "Quality Profile";
|
public override string ImplementationName => "Quality Profile";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Quality Profile", Type = FieldType.QualityProfile)]
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationQualityProfile", Type = FieldType.QualityProfile)]
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
|||||||
public override int Order => 1;
|
public override int Order => 1;
|
||||||
public override string ImplementationName => "Root Folder";
|
public override string ImplementationName => "Root Folder";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Root Folder", Type = FieldType.RootFolder)]
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationRootFolder", Type = FieldType.RootFolder)]
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
|||||||
public override int Order => 1;
|
public override int Order => 1;
|
||||||
public override string ImplementationName => "Runtime";
|
public override string ImplementationName => "Runtime";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Minimum Runtime", Type = FieldType.Number)]
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationMinimumRuntime", Type = FieldType.Number, Unit = "minutes")]
|
||||||
public int Min { get; set; }
|
public int Min { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Maximum Runtime", Type = FieldType.Number)]
|
[FieldDefinition(2, Label = "AutoTaggingSpecificationMaximumRuntime", Type = FieldType.Number, Unit = "minutes")]
|
||||||
public int Max { get; set; }
|
public int Max { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.AutoTagging.Specifications
|
||||||
|
{
|
||||||
|
public class StatusSpecificationValidator : AbstractValidator<StatusSpecification>
|
||||||
|
{
|
||||||
|
public StatusSpecificationValidator()
|
||||||
|
{
|
||||||
|
RuleFor(c => c.Status).Custom((statusType, context) =>
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(typeof(MovieStatusType), statusType))
|
||||||
|
{
|
||||||
|
context.AddFailure($"Invalid status type condition value: {statusType}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StatusSpecification : AutoTaggingSpecificationBase
|
||||||
|
{
|
||||||
|
private static readonly StatusSpecificationValidator Validator = new ();
|
||||||
|
|
||||||
|
public override int Order => 1;
|
||||||
|
public override string ImplementationName => "Status";
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationStatus", Type = FieldType.Select, SelectOptions = typeof(MovieStatusType))]
|
||||||
|
public int Status { get; set; }
|
||||||
|
|
||||||
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
{
|
||||||
|
return movie?.MovieMetadata?.Value?.Status == (MovieStatusType)Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override NzbDroneValidationResult Validate()
|
||||||
|
{
|
||||||
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,10 +23,10 @@ namespace NzbDrone.Core.AutoTagging.Specifications
|
|||||||
public override int Order => 1;
|
public override int Order => 1;
|
||||||
public override string ImplementationName => "Year";
|
public override string ImplementationName => "Year";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)]
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationMinimumYear", Type = FieldType.Number)]
|
||||||
public int Min { get; set; }
|
public int Min { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)]
|
[FieldDefinition(2, Label = "AutoTaggingSpecificationMaximumYear", Type = FieldType.Number)]
|
||||||
public int Max { get; set; }
|
public int Max { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
|||||||
@@ -66,12 +66,19 @@ namespace NzbDrone.Core.Backup
|
|||||||
{
|
{
|
||||||
_logger.ProgressInfo("Starting Backup");
|
_logger.ProgressInfo("Starting Backup");
|
||||||
|
|
||||||
|
var backupFolder = GetBackupFolder(backupType);
|
||||||
|
|
||||||
_diskProvider.EnsureFolder(_backupTempFolder);
|
_diskProvider.EnsureFolder(_backupTempFolder);
|
||||||
_diskProvider.EnsureFolder(GetBackupFolder(backupType));
|
_diskProvider.EnsureFolder(backupFolder);
|
||||||
|
|
||||||
|
if (!_diskProvider.FolderWritable(backupFolder))
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
|
||||||
|
}
|
||||||
|
|
||||||
var dateNow = DateTime.Now;
|
var dateNow = DateTime.Now;
|
||||||
var backupFilename = $"radarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
|
var backupFilename = $"radarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
|
||||||
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
|
var backupPath = Path.Combine(backupFolder, backupFilename);
|
||||||
|
|
||||||
Cleanup();
|
Cleanup();
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||||||
public override int Order => 3;
|
public override int Order => 3;
|
||||||
public override string ImplementationName => "Language";
|
public override string ImplementationName => "Language";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationLanguage", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))]
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "CustomFormatsSpecificationExceptLanguage", HelpText = "CustomFormatsSpecificationExceptLanguageHelpText", Type = FieldType.Checkbox)]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationExceptLanguage", HelpText = "CustomFormatsSpecificationExceptLanguageHelpText", Type = FieldType.Checkbox)]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||||||
public override int Order => 7;
|
public override int Order => 7;
|
||||||
public override string ImplementationName => "Quality Modifier";
|
public override string ImplementationName => "Quality Modifier";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Quality Modifier", Type = FieldType.Select, SelectOptions = typeof(Modifier))]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationQualityModifier", Type = FieldType.Select, SelectOptions = typeof(Modifier))]
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||||||
public override int Order => 6;
|
public override int Order => 6;
|
||||||
public override string ImplementationName => "Resolution";
|
public override string ImplementationName => "Resolution";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Resolution", Type = FieldType.Select, SelectOptions = typeof(Resolution))]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationResolution", Type = FieldType.Select, SelectOptions = typeof(Resolution))]
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ namespace NzbDrone.Core.CustomFormats
|
|||||||
public override int Order => 8;
|
public override int Order => 8;
|
||||||
public override string ImplementationName => "Size";
|
public override string ImplementationName => "Size";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Minimum Size", HelpText = "Release must be greater than this size", Unit = "GB", Type = FieldType.Number)]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationMinimumSize", HelpText = "CustomFormatsSpecificationMinimumSizeHelpText", Unit = "GB", Type = FieldType.Number)]
|
||||||
public double Min { get; set; }
|
public double Min { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Maximum Size", HelpText = "Release must be less than or equal to this size", Unit = "GB", Type = FieldType.Number)]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationMaximumSize", HelpText = "CustomFormatsSpecificationMaximumSizeHelpText", Unit = "GB", Type = FieldType.Number)]
|
||||||
public double Max { get; set; }
|
public double Max { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||||||
public override int Order => 5;
|
public override int Order => 5;
|
||||||
public override string ImplementationName => "Source";
|
public override string ImplementationName => "Source";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Source", Type = FieldType.Select, SelectOptions = typeof(QualitySource))]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationSource", Type = FieldType.Select, SelectOptions = typeof(QualitySource))]
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ namespace NzbDrone.Core.CustomFormats
|
|||||||
public override int Order => 10;
|
public override int Order => 10;
|
||||||
public override string ImplementationName => "Year";
|
public override string ImplementationName => "Year";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)]
|
[FieldDefinition(1, Label = "CustomFormatsSpecificationMinimumYear", Type = FieldType.Number)]
|
||||||
public int Min { get; set; }
|
public int Min { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)]
|
[FieldDefinition(2, Label = "CustomFormatsSpecificationMaximumYear", Type = FieldType.Number)]
|
||||||
public int Max { get; set; }
|
public int Max { get; set; }
|
||||||
|
|
||||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using Dapper;
|
||||||
|
using FluentMigrator;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(241)]
|
||||||
|
public class stevenlu_update_url : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Execute.WithConnection(FixStevenLuListsLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FixStevenLuListsLink(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
var updated = new List<object>();
|
||||||
|
|
||||||
|
using (var getStevenLuListCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
getStevenLuListCmd.Transaction = tran;
|
||||||
|
getStevenLuListCmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"ImportLists\" WHERE \"ConfigContract\" = 'StevenLuSettings'";
|
||||||
|
|
||||||
|
using var reader = getStevenLuListCmd.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var id = reader.GetInt32(0);
|
||||||
|
var settings = Json.Deserialize<JObject>(reader.GetString(1));
|
||||||
|
|
||||||
|
var link = settings.Value<string>("link");
|
||||||
|
|
||||||
|
if (link.IsNotNullOrWhiteSpace() && link.StartsWith("https://s3.amazonaws.com/popular-movies"))
|
||||||
|
{
|
||||||
|
settings["link"] = "https://popular-movies-data.stevenlu.com/movies.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
updated.Add(new
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Settings = settings.ToJson()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateSql = "UPDATE \"ImportLists\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
|
||||||
|
conn.Execute(updateSql, updated, transaction: tran);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
namespace NzbDrone.Core.DecisionEngine
|
|
||||||
{
|
|
||||||
public class Decision
|
|
||||||
{
|
|
||||||
public bool Accepted { get; private set; }
|
|
||||||
public string Reason { get; private set; }
|
|
||||||
|
|
||||||
private static readonly Decision AcceptDecision = new Decision { Accepted = true };
|
|
||||||
private Decision()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decision Accept()
|
|
||||||
{
|
|
||||||
return AcceptDecision;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decision Reject(string reason, params object[] args)
|
|
||||||
{
|
|
||||||
return Reject(string.Format(reason, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Decision Reject(string reason)
|
|
||||||
{
|
|
||||||
return new Decision
|
|
||||||
{
|
|
||||||
Accepted = false,
|
|
||||||
Reason = reason
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
{
|
{
|
||||||
public RemoteMovie RemoteMovie { get; private set; }
|
public RemoteMovie RemoteMovie { get; private set; }
|
||||||
|
|
||||||
public IEnumerable<Rejection> Rejections { get; private set; }
|
public IEnumerable<DownloadRejection> Rejections { get; private set; }
|
||||||
|
|
||||||
public bool Approved => !Rejections.Any();
|
public bool Approved => !Rejections.Any();
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadDecision(RemoteMovie movie, params Rejection[] rejections)
|
public DownloadDecision(RemoteMovie movie, params DownloadRejection[] rejections)
|
||||||
{
|
{
|
||||||
RemoteMovie = movie;
|
RemoteMovie = movie;
|
||||||
Rejections = rejections.ToList();
|
Rejections = rejections.ToList();
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
return 10;
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return Math.Round(Math.Log10(age)) * -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
|
|
||||||
public class DownloadDecisionMaker : IMakeDownloadDecision
|
public class DownloadDecisionMaker : IMakeDownloadDecision
|
||||||
{
|
{
|
||||||
private readonly IEnumerable<IDecisionEngineSpecification> _specifications;
|
private readonly IEnumerable<IDownloadDecisionEngineSpecification> _specifications;
|
||||||
private readonly IParsingService _parsingService;
|
private readonly IParsingService _parsingService;
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||||
private readonly IRemoteMovieAggregationService _aggregationService;
|
private readonly IRemoteMovieAggregationService _aggregationService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public DownloadDecisionMaker(IEnumerable<IDecisionEngineSpecification> specifications,
|
public DownloadDecisionMaker(IEnumerable<IDownloadDecisionEngineSpecification> specifications,
|
||||||
IParsingService parsingService,
|
IParsingService parsingService,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
ICustomFormatCalculationService formatCalculator,
|
ICustomFormatCalculationService formatCalculator,
|
||||||
@@ -85,9 +85,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
|
|
||||||
if (remoteMovie.Movie == null)
|
if (remoteMovie.Movie == null)
|
||||||
{
|
{
|
||||||
var reason = "Unknown Movie";
|
decision = new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.UnknownMovie, "Unknown Movie. Unable to identify correct movie using release name."));
|
||||||
|
|
||||||
decision = new DownloadDecision(remoteMovie, new Rejection(reason));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -96,6 +94,8 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(remoteMovie, remoteMovie.Release.Size);
|
remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(remoteMovie, remoteMovie.Release.Size);
|
||||||
remoteMovie.CustomFormatScore = remoteMovie?.Movie?.QualityProfile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0;
|
remoteMovie.CustomFormatScore = remoteMovie?.Movie?.QualityProfile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0;
|
||||||
|
|
||||||
|
_logger.Trace("Custom Format Score of '{0}' [{1}] calculated for '{2}'", remoteMovie.CustomFormatScore, remoteMovie.CustomFormats?.ConcatToString(), report.Title);
|
||||||
|
|
||||||
remoteMovie.DownloadAllowed = remoteMovie.Movie != null;
|
remoteMovie.DownloadAllowed = remoteMovie.Movie != null;
|
||||||
decision = GetDecisionForReport(remoteMovie, searchCriteria);
|
decision = GetDecisionForReport(remoteMovie, searchCriteria);
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
Languages = parsedMovieInfo.Languages
|
Languages = parsedMovieInfo.Languages
|
||||||
};
|
};
|
||||||
|
|
||||||
decision = new DownloadDecision(remoteMovie, new Rejection("Unable to parse release"));
|
decision = new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.UnableToParse, "Unable to parse release"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
_logger.Error(e, "Couldn't process release.");
|
_logger.Error(e, "Couldn't process release.");
|
||||||
|
|
||||||
var remoteMovie = new RemoteMovie { Release = report };
|
var remoteMovie = new RemoteMovie { Release = report };
|
||||||
decision = new DownloadDecision(remoteMovie, new Rejection("Unexpected error processing release"));
|
decision = new DownloadDecision(remoteMovie, new DownloadRejection(DownloadRejectionReason.Error, "Unexpected error processing release"));
|
||||||
}
|
}
|
||||||
|
|
||||||
reportNumber++;
|
reportNumber++;
|
||||||
@@ -173,7 +173,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
|
|
||||||
private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null)
|
private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null)
|
||||||
{
|
{
|
||||||
var reasons = Array.Empty<Rejection>();
|
var reasons = Array.Empty<DownloadRejection>();
|
||||||
|
|
||||||
foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key))
|
foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key))
|
||||||
{
|
{
|
||||||
@@ -190,7 +190,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
return new DownloadDecision(remoteMovie, reasons.ToArray());
|
return new DownloadDecision(remoteMovie, reasons.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Rejection EvaluateSpec(IDecisionEngineSpecification spec, RemoteMovie remoteMovie, SearchCriteriaBase searchCriteriaBase = null)
|
private DownloadRejection EvaluateSpec(IDownloadDecisionEngineSpecification spec, RemoteMovie remoteMovie, SearchCriteriaBase searchCriteriaBase = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -198,7 +198,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
|
|
||||||
if (!result.Accepted)
|
if (!result.Accepted)
|
||||||
{
|
{
|
||||||
return new Rejection(result.Reason, spec.Type);
|
return new DownloadRejection(result.Reason, result.Message, spec.Type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (NotImplementedException)
|
catch (NotImplementedException)
|
||||||
@@ -210,7 +210,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
e.Data.Add("report", remoteMovie.Release.ToJson());
|
e.Data.Add("report", remoteMovie.Release.ToJson());
|
||||||
e.Data.Add("parsed", remoteMovie.ParsedMovieInfo.ToJson());
|
e.Data.Add("parsed", remoteMovie.ParsedMovieInfo.ToJson());
|
||||||
_logger.Error(e, "Couldn't evaluate decision on {0}, with spec: {1}", remoteMovie.Release.Title, spec.GetType().Name);
|
_logger.Error(e, "Couldn't evaluate decision on {0}, with spec: {1}", remoteMovie.Release.Title, spec.GetType().Name);
|
||||||
return new Rejection($"{spec.GetType().Name}: {e.Message}");
|
return new DownloadRejection(DownloadRejectionReason.DecisionError, $"{spec.GetType().Name}: {e.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Core.DecisionEngine;
|
||||||
|
|
||||||
|
public class DownloadRejection : Rejection<DownloadRejectionReason>
|
||||||
|
{
|
||||||
|
public DownloadRejection(DownloadRejectionReason reason, string message, RejectionType type = RejectionType.Permanent)
|
||||||
|
: base(reason, message, type)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
namespace NzbDrone.Core.DecisionEngine;
|
||||||
|
|
||||||
|
public enum DownloadRejectionReason
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
UnknownMovie,
|
||||||
|
UnableToParse,
|
||||||
|
Error,
|
||||||
|
DecisionError,
|
||||||
|
Availability,
|
||||||
|
MinimumAgeDelay,
|
||||||
|
MovieNotMonitored,
|
||||||
|
HistoryRecentCutoffMet,
|
||||||
|
HistoryCdhDisabledCutoffMet,
|
||||||
|
HistoryHigherPreference,
|
||||||
|
HistoryHigherRevision,
|
||||||
|
HistoryCutoffMet,
|
||||||
|
HistoryCustomFormatCutoffMet,
|
||||||
|
HistoryCustomFormatScore,
|
||||||
|
HistoryCustomFormatScoreIncrement,
|
||||||
|
HistoryUpgradesNotAllowed,
|
||||||
|
NoMatchingTag,
|
||||||
|
PropersDisabled,
|
||||||
|
ProperForOldFile,
|
||||||
|
WrongMovie,
|
||||||
|
UnknownRuntime,
|
||||||
|
BelowMinimumSize,
|
||||||
|
AboveMaximumSize,
|
||||||
|
AlreadyImportedSameHash,
|
||||||
|
AlreadyImportedSameName,
|
||||||
|
IndexerDisabled,
|
||||||
|
Blocklisted,
|
||||||
|
CustomFormatMinimumScore,
|
||||||
|
MinimumFreeSpace,
|
||||||
|
HardcodeSubtitles,
|
||||||
|
WantedLanguage,
|
||||||
|
MaximumSizeExceeded,
|
||||||
|
MinimumAge,
|
||||||
|
MaximumAge,
|
||||||
|
Sample,
|
||||||
|
ProtocolDisabled,
|
||||||
|
QualityNotWanted,
|
||||||
|
QualityUpgradesDisabled,
|
||||||
|
QueueHigherPreference,
|
||||||
|
QueueHigherRevision,
|
||||||
|
QueueCutoffMet,
|
||||||
|
QueueCustomFormatCutoffMet,
|
||||||
|
QueueCustomFormatScore,
|
||||||
|
QueueCustomFormatScoreIncrement,
|
||||||
|
QueueUpgradesNotAllowed,
|
||||||
|
QueuePropersDisabled,
|
||||||
|
Raw,
|
||||||
|
MustContainMissing,
|
||||||
|
MustNotContainPresent,
|
||||||
|
RepackDisabled,
|
||||||
|
RepackUnknownReleaseGroup,
|
||||||
|
RepackReleaseGroupDoesNotMatch,
|
||||||
|
RequiredFlags,
|
||||||
|
MinimumSeeders,
|
||||||
|
DiskHigherPreference,
|
||||||
|
DiskHigherRevision,
|
||||||
|
DiskCutoffMet,
|
||||||
|
DiskCustomFormatCutoffMet,
|
||||||
|
DiskCustomFormatScore,
|
||||||
|
DiskCustomFormatScoreIncrement,
|
||||||
|
DiskUpgradesNotAllowed
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace NzbDrone.Core.DecisionEngine
|
||||||
|
{
|
||||||
|
public class DownloadSpecDecision
|
||||||
|
{
|
||||||
|
public bool Accepted { get; private set; }
|
||||||
|
public DownloadRejectionReason Reason { get; set; }
|
||||||
|
public string Message { get; private set; }
|
||||||
|
|
||||||
|
private static readonly DownloadSpecDecision AcceptDownloadSpecDecision = new () { Accepted = true };
|
||||||
|
private DownloadSpecDecision()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadSpecDecision Accept()
|
||||||
|
{
|
||||||
|
return AcceptDownloadSpecDecision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadSpecDecision Reject(DownloadRejectionReason reason, string message, params object[] args)
|
||||||
|
{
|
||||||
|
return Reject(reason, string.Format(message, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadSpecDecision Reject(DownloadRejectionReason reason, string message)
|
||||||
|
{
|
||||||
|
return new DownloadSpecDecision
|
||||||
|
{
|
||||||
|
Accepted = false,
|
||||||
|
Reason = reason,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user