mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-09 15:01:39 -04:00
Compare commits
190 Commits
update-cha
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
676dcbae0e | ||
|
|
5a7a9db7ed | ||
|
|
182cda47b0 | ||
|
|
294d95fae4 | ||
|
|
0e3f871e0e | ||
|
|
b0f5f02edc | ||
|
|
2afe6af5a6 | ||
|
|
e2eaf91aa7 | ||
|
|
0e1c2c3c50 | ||
|
|
69cf2e89a6 | ||
|
|
9830230589 | ||
|
|
b1f0b2c216 | ||
|
|
7c6858ecfb | ||
|
|
ee32d42c94 | ||
|
|
3390df4085 | ||
|
|
01bc5f6fc8 | ||
|
|
2d867a6cb6 | ||
|
|
411f8866ec | ||
|
|
5316382113 | ||
|
|
8fe81b428a | ||
|
|
43a2a2d335 | ||
|
|
5c8b58c30d | ||
|
|
131a223bb9 | ||
|
|
dfaab639bf | ||
|
|
c7be63d48f | ||
|
|
2958faf4a8 | ||
|
|
4280df8b61 | ||
|
|
1f91be6407 | ||
|
|
eb43a3c2d0 | ||
|
|
20c7e84676 | ||
|
|
691a8955fe | ||
|
|
53a9c849cb | ||
|
|
856a55a9c9 | ||
|
|
43cd536746 | ||
|
|
4a205d8041 | ||
|
|
2525ac2d1a | ||
|
|
e5ceb20a83 | ||
|
|
0f6b11f55d | ||
|
|
500bc3a571 | ||
|
|
e6567d0365 | ||
|
|
dbca393772 | ||
|
|
9662495fa2 | ||
|
|
76f0c54b3c | ||
|
|
d7ff92115c | ||
|
|
54a49d6878 | ||
|
|
a8362511f9 | ||
|
|
b9f2b3e06f | ||
|
|
d995bc5a7e | ||
|
|
8886162bba | ||
|
|
eb9eb4ec64 | ||
|
|
f910a8fde7 | ||
|
|
f6904608a7 | ||
|
|
0c79548fc4 | ||
|
|
362e664ce6 | ||
|
|
c2cbfb274a | ||
|
|
9b3770a018 | ||
|
|
9db6289693 | ||
|
|
8a63f6ae37 | ||
|
|
069b18e5e3 | ||
|
|
f05333db51 | ||
|
|
f50e8f631e | ||
|
|
b9886cd11c | ||
|
|
9f3eecb2a9 | ||
|
|
52c24a4333 | ||
|
|
9bc31b46fa | ||
|
|
f4d8e113c1 | ||
|
|
1e1a4240d1 | ||
|
|
8fb53df4af | ||
|
|
f6dd600d2b | ||
|
|
40a15d59e0 | ||
|
|
c7baa66de2 | ||
|
|
2be70f5001 | ||
|
|
da857701f6 | ||
|
|
828d7eb1f3 | ||
|
|
b3a056edf9 | ||
|
|
98437c3cac | ||
|
|
c5616c5ba1 | ||
|
|
61979bff7a | ||
|
|
90d0d8bec8 | ||
|
|
2d814ecd20 | ||
|
|
6542119402 | ||
|
|
b9185574f3 | ||
|
|
99e0d42b71 | ||
|
|
d01fa5e6a4 | ||
|
|
2ce9d099e1 | ||
|
|
12829580e5 | ||
|
|
dadd796737 | ||
|
|
a3f508b8d4 | ||
|
|
1ab3df03a3 | ||
|
|
5558e10711 | ||
|
|
573405bae7 | ||
|
|
43d77308f9 | ||
|
|
b3c3f7ddae | ||
|
|
dd5bc41eda | ||
|
|
c8ab4f8c68 | ||
|
|
a4ddae0ccc | ||
|
|
d730161800 | ||
|
|
66c1af0555 | ||
|
|
dca00db317 | ||
|
|
812e5ac5a3 | ||
|
|
d01bae92bf | ||
|
|
1a6bf51741 | ||
|
|
f3e7843150 | ||
|
|
886b9b1c05 | ||
|
|
d8891ee4ea | ||
|
|
192dd9c137 | ||
|
|
b549fddf95 | ||
|
|
c1f538ed97 | ||
|
|
e72f8097fb | ||
|
|
3eec088306 | ||
|
|
ad097dd1a2 | ||
|
|
b4b38a5318 | ||
|
|
b0717a0803 | ||
|
|
4d1d08d345 | ||
|
|
e689817508 | ||
|
|
3b191caf16 | ||
|
|
cc6ca0b067 | ||
|
|
57cb63fb18 | ||
|
|
20f709d22a | ||
|
|
5d8775ac96 | ||
|
|
4890972e16 | ||
|
|
40dc808f61 | ||
|
|
97077e09d2 | ||
|
|
9ba7027d00 | ||
|
|
9903e70925 | ||
|
|
3a6f3666f5 | ||
|
|
915c66be50 | ||
|
|
70b22e483a | ||
|
|
cad1191da5 | ||
|
|
43910af127 | ||
|
|
f01c477b81 | ||
|
|
0054318658 | ||
|
|
03a3f4522a | ||
|
|
3d3562dcda | ||
|
|
7a079c5e0c | ||
|
|
4d70798f2f | ||
|
|
d55864f869 | ||
|
|
3c41c84fb0 | ||
|
|
eae9a6d6e0 | ||
|
|
867f8f5835 | ||
|
|
0c81387cfb | ||
|
|
c5fb5200de | ||
|
|
cc306fcd36 | ||
|
|
2bb7984961 | ||
|
|
21e605452a | ||
|
|
476f5b5bfd | ||
|
|
b6920cfe82 | ||
|
|
e89b98d0f6 | ||
|
|
1db690ad39 | ||
|
|
d5c524719b | ||
|
|
ced6586860 | ||
|
|
8b3019821a | ||
|
|
16ed68d5de | ||
|
|
098a893083 | ||
|
|
548e3400b5 | ||
|
|
5c31e3f1a2 | ||
|
|
7404793dcf | ||
|
|
d8af17ce3d | ||
|
|
44c912f02d | ||
|
|
b104368e23 | ||
|
|
aa0104b6bc | ||
|
|
69fcd8ec94 | ||
|
|
a59928c66a | ||
|
|
1cb7ae11a2 | ||
|
|
ca519047dd | ||
|
|
f15a6abde0 | ||
|
|
2aacebc938 | ||
|
|
120e9b673e | ||
|
|
0a77a13fa8 | ||
|
|
383f9647c3 | ||
|
|
7f7c672b93 | ||
|
|
2690ad8fe1 | ||
|
|
801204b6de | ||
|
|
cb9514abaf | ||
|
|
fd22cb44f6 | ||
|
|
2d68716376 | ||
|
|
b97e76c8b8 | ||
|
|
bfad4a8cd1 | ||
|
|
61f05710f5 | ||
|
|
a8ecefd91f | ||
|
|
e3468daba0 | ||
|
|
f2a7d0d520 | ||
|
|
43257f0726 | ||
|
|
6c2bf860fe | ||
|
|
3a1d848e59 | ||
|
|
f6590e71d2 | ||
|
|
586dd737fd | ||
|
|
fa84dda38c | ||
|
|
4a233ce915 | ||
|
|
ffdd9a1708 |
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"paths": [
|
||||
"frontend/src/**/*.js",
|
||||
"src/NzbDrone.Core/Localization/Core/*.json"
|
||||
"frontend/src/**/*.js"
|
||||
],
|
||||
"ignored": [
|
||||
"**/node_modules/**/*"
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: radarr
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: radarr
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -42,12 +42,14 @@ body:
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
- **Database**: Sqlite 3.36.0
|
||||
value: |
|
||||
- OS:
|
||||
- Radarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
- Radarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
- Database:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
4
.github/workflows/azuresync.yml
vendored
4
.github/workflows/azuresync.yml
vendored
@@ -7,8 +7,12 @@ on:
|
||||
|
||||
concurrency: azuresync-${{ github.event.issue.number }}
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
alert:
|
||||
permissions:
|
||||
issues: write # to update issue body
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
|
||||
5
.github/workflows/lock.yml
vendored
5
.github/workflows/lock.yml
vendored
@@ -5,8 +5,13 @@ on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
lock:
|
||||
permissions:
|
||||
issues: write # to lock issues (dessant/lock-threads)
|
||||
pull-requests: write # to lock PRs (dessant/lock-threads)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2
|
||||
|
||||
4
.github/workflows/support.yml
vendored
4
.github/workflows/support.yml
vendored
@@ -4,8 +4,12 @@ on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
support:
|
||||
permissions:
|
||||
issues: write # to modify issues
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2
|
||||
|
||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<development@radarr.video>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.2.0'
|
||||
majorVersion: '4.3.1'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.300'
|
||||
dotnetVersion: '6.0.400'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
@@ -173,7 +173,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
@@ -550,7 +549,7 @@ stages:
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
@@ -577,6 +576,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=radarr \
|
||||
-e POSTGRES_USER=radarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -687,7 +687,7 @@ stages:
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
@@ -722,6 +722,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=radarr \
|
||||
-e POSTGRES_USER=radarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -760,7 +761,7 @@ stages:
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '/$(pattern)'
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- bash: |
|
||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||
@@ -976,7 +977,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
@@ -1143,4 +1143,5 @@ stages:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
DISCORDTHREADID: $(discordThreadId)
|
||||
|
||||
|
||||
@@ -223,7 +223,6 @@ module.exports = (env) => {
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 24096,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
@@ -233,12 +232,11 @@ module.exports = (env) => {
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|eot?#iefix|gif|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
limit: 24096,
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.customFormatScore {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import styles from './HistoryRow.css';
|
||||
@@ -168,6 +169,17 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.customFormatScore}
|
||||
>
|
||||
{formatCustomFormatScore(data.customFormatScore)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseGroup') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
@@ -190,7 +190,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
|
||||
<div>
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
iconSize={13}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.movie {
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -6,3 +7,19 @@
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
flex: 1 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tmdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.tmdbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ImportMovieTitle from './ImportMovieTitle';
|
||||
import styles from './ImportMovieSearchResult.css';
|
||||
|
||||
@@ -18,6 +20,7 @@ class ImportMovieSearchResult extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
title,
|
||||
year,
|
||||
studio,
|
||||
@@ -25,17 +28,30 @@ class ImportMovieSearchResult extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.movie}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={studio}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</Link>
|
||||
<div className={styles.container}>
|
||||
<Link
|
||||
className={styles.movie}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={studio}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={styles.tmdbLink}
|
||||
to={`https://www.themoviedb.org/movie/${tmdbId}`}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tmdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={16}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
|
||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
@@ -22,6 +25,9 @@ class CollectionFooter extends Component {
|
||||
this.state = {
|
||||
monitor: NO_CHANGE,
|
||||
monitored: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
destinationRootFolder: null
|
||||
};
|
||||
}
|
||||
@@ -36,7 +42,10 @@ class CollectionFooter extends Component {
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
monitor: NO_CHANGE
|
||||
monitor: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,7 +64,10 @@ class CollectionFooter extends Component {
|
||||
onUpdateSelectedPress = () => {
|
||||
const {
|
||||
monitor,
|
||||
monitored
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
rootFolderPath
|
||||
} = this.state;
|
||||
|
||||
const changes = {};
|
||||
@@ -68,6 +80,18 @@ class CollectionFooter extends Component {
|
||||
changes.monitor = monitor;
|
||||
}
|
||||
|
||||
if (qualityProfileId !== NO_CHANGE) {
|
||||
changes.qualityProfileId = qualityProfileId;
|
||||
}
|
||||
|
||||
if (minimumAvailability !== NO_CHANGE) {
|
||||
changes.minimumAvailability = minimumAvailability;
|
||||
}
|
||||
|
||||
if (rootFolderPath !== NO_CHANGE) {
|
||||
changes.rootFolderPath = rootFolderPath;
|
||||
}
|
||||
|
||||
this.props.onUpdateSelectedPress(changes);
|
||||
};
|
||||
|
||||
@@ -82,7 +106,10 @@ class CollectionFooter extends Component {
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitor
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
rootFolderPath
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
@@ -125,6 +152,52 @@ class CollectionFooter extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('MinimumAvailability')}
|
||||
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<AvailabilitySelectInput
|
||||
name="minimumAvailability"
|
||||
value={minimumAvailability}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<CollectionFooterLabel
|
||||
|
||||
@@ -115,7 +115,7 @@ class EditCollectionModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Folder')}</FormLabel>
|
||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Slider from 'react-slick';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import { Navigation } from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -17,8 +18,9 @@ import CollectionMovieConnector from './CollectionMovieConnector';
|
||||
import CollectionMovieLabelConnector from './CollectionMovieLabelConnector';
|
||||
import styles from './CollectionOverview.css';
|
||||
|
||||
import 'slick-carousel/slick/slick.css';
|
||||
import 'slick-carousel/slick/slick-theme.css';
|
||||
// Import Swiper styles
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
@@ -52,8 +54,12 @@ class CollectionOverview extends Component {
|
||||
//
|
||||
// Control
|
||||
|
||||
setSliderRef = (ref) => {
|
||||
this.setState({ slider: ref });
|
||||
setSliderPrevRef = (ref) => {
|
||||
this._swiperPrevRef = ref;
|
||||
};
|
||||
|
||||
setSliderNextRef = (ref) => {
|
||||
this._swiperNextRef = ref;
|
||||
};
|
||||
|
||||
//
|
||||
@@ -120,15 +126,6 @@ class CollectionOverview extends Component {
|
||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||
const overviewHeight = contentHeight - titleRowHeight - posterHeight;
|
||||
|
||||
const sliderSettings = {
|
||||
arrows: false,
|
||||
dots: false,
|
||||
infinite: false,
|
||||
slidesToShow: 1,
|
||||
slidesToScroll: 1,
|
||||
variableWidth: true
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
@@ -166,19 +163,21 @@ class CollectionOverview extends Component {
|
||||
{
|
||||
showPosters &&
|
||||
<div className={styles.navigationButtons}>
|
||||
<IconButton
|
||||
name={icons.ARROW_LEFT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickPrev}
|
||||
size={20}
|
||||
/>
|
||||
<span ref={this.setSliderPrevRef}>
|
||||
<IconButton
|
||||
name={icons.ARROW_LEFT}
|
||||
title={translate('ScrollMovies')}
|
||||
size={20}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<IconButton
|
||||
name={icons.ARROW_RIGHT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickNext}
|
||||
size={20}
|
||||
/>
|
||||
<span ref={this.setSliderNextRef}>
|
||||
<IconButton
|
||||
name={icons.ARROW_RIGHT}
|
||||
title={translate('ScrollMovies')}
|
||||
size={20}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -270,9 +269,23 @@ class CollectionOverview extends Component {
|
||||
{
|
||||
showPosters ?
|
||||
<div className={styles.sliderContainer}>
|
||||
<Slider ref={this.setSliderRef} {...sliderSettings}>
|
||||
<Swiper
|
||||
slidesPerView='auto'
|
||||
spaceBetween={10}
|
||||
slidesPerGroup={3}
|
||||
loop={false}
|
||||
loopFillGroupWithBlank={true}
|
||||
className="mySwiper"
|
||||
modules={[Navigation]}
|
||||
onInit={(swiper) => {
|
||||
swiper.params.navigation.prevEl = this._swiperPrevRef;
|
||||
swiper.params.navigation.nextEl = this._swiperNextRef;
|
||||
swiper.navigation.init();
|
||||
swiper.navigation.update();
|
||||
}}
|
||||
>
|
||||
{movies.map((movie) => (
|
||||
<div className={styles.movie} key={movie.tmdbId}>
|
||||
<SwiperSlide key={movie.tmdbId} style={{ width: posterWidth }}>
|
||||
<CollectionMovieConnector
|
||||
key={movie.tmdbId}
|
||||
posterWidth={posterWidth}
|
||||
@@ -281,9 +294,9 @@ class CollectionOverview extends Component {
|
||||
collectionId={id}
|
||||
{...movie}
|
||||
/>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Slider>
|
||||
</Swiper>
|
||||
</div> :
|
||||
<div className={styles.labelsContainer}>
|
||||
{movies.map((movie) => (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -22,11 +22,11 @@ class ImdbRating extends PureComponent {
|
||||
let ratingString = '0.0';
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value}`;
|
||||
ratingString = `${rating.value.toFixed(1)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span title={`${rating.votes} votes`}>
|
||||
<span title={`${rating ? rating.votes : 0} votes`}>
|
||||
{
|
||||
!hideIcon &&
|
||||
<img
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.jumpBar {
|
||||
z-index: $pageJumpBarZIndex;
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
padding-top: 4px;
|
||||
width: $toolbarButtonWidth;
|
||||
min-width: $toolbarButtonWidth;
|
||||
width: min-content;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -21,9 +21,11 @@ class RottenTomatoRating extends PureComponent {
|
||||
const rating = ratings.rottenTomatoes;
|
||||
|
||||
let ratingString = '0%';
|
||||
let ratingImage = rtFresh;
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value}%`;
|
||||
ratingImage = rating.value > 50 ? rtFresh : rtRotten;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,7 +34,7 @@ class RottenTomatoRating extends PureComponent {
|
||||
!hideIcon &&
|
||||
<img
|
||||
className={styles.image}
|
||||
src={rating.value > 50 ? rtFresh : rtRotten}
|
||||
src={ratingImage}
|
||||
style={{
|
||||
height: `${iconSize}px`
|
||||
}}
|
||||
|
||||
@@ -192,7 +192,7 @@ class TableOptionsModal extends Component {
|
||||
<TableOptionsColumnDragSource
|
||||
key={name}
|
||||
name={name}
|
||||
label={label || columnLabel}
|
||||
label={columnLabel || label}
|
||||
isVisible={isVisible}
|
||||
isModifiable={true}
|
||||
index={index}
|
||||
@@ -210,7 +210,7 @@ class TableOptionsModal extends Component {
|
||||
<TableOptionsColumn
|
||||
key={name}
|
||||
name={name}
|
||||
label={label || columnLabel}
|
||||
label={columnLabel || label}
|
||||
isVisible={isVisible}
|
||||
index={index}
|
||||
isModifiable={false}
|
||||
|
||||
@@ -22,7 +22,7 @@ class TmdbRating extends PureComponent {
|
||||
let ratingString = '0%';
|
||||
|
||||
if (rating) {
|
||||
ratingString = `${rating.value * 10}%`;
|
||||
ratingString = `${(rating.value * 10).toFixed()}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
@@ -111,7 +111,7 @@ function DiscoverMoviePosterInfo(props) {
|
||||
if (sortKey === 'ratings' && ratings) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImportListListConnector from 'Components/ImportListListConnector';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
@@ -8,6 +7,7 @@ import Link from 'Components/Link/Link';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
@@ -245,7 +245,7 @@ class DiscoverMovieRow extends Component {
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import {
|
||||
faArrowCircleLeft as fasArrowCircleLeft,
|
||||
faArrowCircleRight as fasArrowCircleRight,
|
||||
faAsterisk as fasAsterisk,
|
||||
faBackward as fasBackward,
|
||||
faBan as fasBan,
|
||||
faBars as fasBars,
|
||||
@@ -154,6 +155,7 @@ export const FILE = farFile;
|
||||
export const FILM = fasFilm;
|
||||
export const FILTER = fasFilter;
|
||||
export const FLAG = fasFlag;
|
||||
export const FOOTNOTE = fasAsterisk;
|
||||
export const FOLDER = farFolder;
|
||||
export const FOLDER_OPEN = fasFolderOpen;
|
||||
export const GENRE = fasTheaterMasks;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectReleaseGroupModalContent.css';
|
||||
|
||||
class SelectReleaseGroupModalContent extends Component {
|
||||
|
||||
@@ -58,7 +59,10 @@ class SelectReleaseGroupModalContent extends Component {
|
||||
{translate('ManualImportSetReleaseGroup')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
|
||||
@@ -67,6 +71,7 @@ class SelectReleaseGroupModalContent extends Component {
|
||||
type={inputTypes.TEXT}
|
||||
name="releaseGroup"
|
||||
value={releaseGroup}
|
||||
autoFocus={true}
|
||||
onChange={this.onReleaseGroupChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieHistoryRow.css';
|
||||
|
||||
@@ -104,6 +105,10 @@ class MovieHistoryRow extends Component {
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell key={name}>
|
||||
{formatCustomFormatScore(data.customFormatScore)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={date}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
@@ -35,6 +36,15 @@ const columns = [
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: 'Custom format score'
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: translate('Date'),
|
||||
|
||||
@@ -102,12 +102,21 @@ function MovieIndexSortMenu(props) {
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="ratings"
|
||||
name="imdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Ratings')}
|
||||
{translate('ImdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="tmdbRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('TmdbRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
|
||||
@@ -77,7 +77,9 @@
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
.ratings {
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -84,7 +84,9 @@
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
.ratings {
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
@@ -349,13 +351,39 @@ class MovieIndexRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
if (name === 'tmdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<HeartRating
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'rottenTomatoesRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<RottenTomatoRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'imdbRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<ImdbRating
|
||||
ratings={ratings}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -36,3 +36,9 @@
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MiddleTruncate from 'react-middle-truncate';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
@@ -124,10 +125,15 @@ class CustomFormat extends Component {
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={styles.label}
|
||||
key={index}
|
||||
kind={kind}
|
||||
>
|
||||
{item.name}
|
||||
<MiddleTruncate
|
||||
text={item.name}
|
||||
start={10}
|
||||
end={14}
|
||||
/>
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ function HostSettings(props) {
|
||||
port,
|
||||
urlBase,
|
||||
instanceName,
|
||||
applicationUrl,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertPath,
|
||||
@@ -90,6 +91,21 @@ function HostSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ApplicationURL')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="applicationUrl"
|
||||
helpText={translate('ApplicationUrlHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...applicationUrl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -166,7 +166,7 @@ function EditImportListModalContent(props) {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Folder')}</FormLabel>
|
||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
|
||||
@@ -16,3 +16,20 @@
|
||||
margin-left: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.footNote {
|
||||
display: flex;
|
||||
color: $helpTextColor;
|
||||
|
||||
.icon {
|
||||
margin-top: 3px;
|
||||
margin-right: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 1px;
|
||||
border: 1px solid $borderColor;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,94 @@ import React, { Component } from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { icons, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NamingOption from './NamingOption';
|
||||
import styles from './NamingModal.css';
|
||||
|
||||
const separatorOptions = [
|
||||
{ key: ' ', value: 'Space ( )' },
|
||||
{ key: '.', value: 'Period (.)' },
|
||||
{ key: '_', value: 'Underscore (_)' },
|
||||
{ key: '-', value: 'Dash (-)' }
|
||||
];
|
||||
|
||||
const caseOptions = [
|
||||
{ key: 'title', value: translate('DefaultCase') },
|
||||
{ key: 'lower', value: translate('LowerCase') },
|
||||
{ key: 'upper', value: translate('UpperCase') }
|
||||
];
|
||||
|
||||
const fileNameTokens = [
|
||||
{
|
||||
token: '{Movie Title} - {Quality Full}',
|
||||
example: 'Movie Title (2010) - HDTV-720p Proper'
|
||||
}
|
||||
];
|
||||
|
||||
const movieTokens = [
|
||||
{ token: '{Movie Title}', example: 'Movie\'s Title' },
|
||||
{ token: '{Movie Title:DE}', example: 'Titel des Films' },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movies Title' },
|
||||
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The' },
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' },
|
||||
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας' },
|
||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||
{ token: '{Movie Collection}', example: 'The Movie Collection' },
|
||||
{ token: '{Movie Certification}', example: 'R' },
|
||||
{ token: '{Release Year}', example: '2009' }
|
||||
];
|
||||
|
||||
const movieIdTokens = [
|
||||
{ token: '{ImdbId}', example: 'tt12345' },
|
||||
{ token: '{TmdbId}', example: '123456' }
|
||||
];
|
||||
|
||||
const qualityTokens = [
|
||||
{ token: '{Quality Full}', example: 'HDTV-720p Proper' },
|
||||
{ token: '{Quality Title}', example: 'HDTV-720p' }
|
||||
];
|
||||
|
||||
const mediaInfoTokens = [
|
||||
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
|
||||
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: 1 },
|
||||
|
||||
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
|
||||
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
|
||||
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: 1 },
|
||||
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: 1 },
|
||||
|
||||
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
|
||||
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
|
||||
{ token: '{MediaInfo VideoDynamicRange}', example: 'HDR' },
|
||||
{ token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' },
|
||||
{ token: '{MediaInfo 3D}', example: '3D' }
|
||||
];
|
||||
|
||||
const releaseGroupTokens = [
|
||||
{ token: '{Release Group}', example: 'Rls Grp' }
|
||||
];
|
||||
|
||||
const editionTokens = [
|
||||
{ token: '{Edition Tags}', example: 'IMAX' }
|
||||
];
|
||||
|
||||
const customFormatTokens = [
|
||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' }
|
||||
];
|
||||
|
||||
const originalTokens = [
|
||||
{ token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' },
|
||||
{ token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' }
|
||||
];
|
||||
|
||||
class NamingModal extends Component {
|
||||
|
||||
//
|
||||
@@ -94,81 +171,6 @@ class NamingModal extends Component {
|
||||
case: tokenCase
|
||||
} = this.state;
|
||||
|
||||
const separatorOptions = [
|
||||
{ key: ' ', value: 'Space ( )' },
|
||||
{ key: '.', value: 'Period (.)' },
|
||||
{ key: '_', value: 'Underscore (_)' },
|
||||
{ key: '-', value: 'Dash (-)' }
|
||||
];
|
||||
|
||||
const caseOptions = [
|
||||
{ key: 'title', value: translate('DefaultCase') },
|
||||
{ key: 'lower', value: translate('LowerCase') },
|
||||
{ key: 'upper', value: translate('UpperCase') }
|
||||
];
|
||||
|
||||
const fileNameTokens = [
|
||||
{
|
||||
token: '{Movie Title} - {Quality Full}',
|
||||
example: 'Movie Title (2010) - HDTV-720p Proper'
|
||||
}
|
||||
];
|
||||
|
||||
const movieTokens = [
|
||||
{ token: '{Movie Title}', example: 'Movie\'s Title' },
|
||||
{ token: '{Movie Title:DE}', example: 'Titel des Films' },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movies Title' },
|
||||
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The' },
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' },
|
||||
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας' },
|
||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||
{ token: '{Movie Collection}', example: 'The Movie Collection' },
|
||||
{ token: '{Movie Certification}', example: 'R' },
|
||||
{ token: '{Release Year}', example: '2009' }
|
||||
];
|
||||
|
||||
const movieIdTokens = [
|
||||
{ token: '{ImdbId}', example: 'tt12345' },
|
||||
{ token: '{TmdbId}', example: '123456' }
|
||||
];
|
||||
|
||||
const qualityTokens = [
|
||||
{ token: '{Quality Full}', example: 'HDTV-720p Proper' },
|
||||
{ token: '{Quality Title}', example: 'HDTV-720p' }
|
||||
];
|
||||
|
||||
const mediaInfoTokens = [
|
||||
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
|
||||
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' },
|
||||
|
||||
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
|
||||
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
|
||||
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]' },
|
||||
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]' },
|
||||
|
||||
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
|
||||
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
|
||||
{ token: '{MediaInfo VideoDynamicRange}', example: 'HDR' },
|
||||
{ token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' }
|
||||
];
|
||||
|
||||
const releaseGroupTokens = [
|
||||
{ token: '{Release Group}', example: 'Rls Grp' }
|
||||
];
|
||||
|
||||
const editionTokens = [
|
||||
{ token: '{Edition Tags}', example: 'IMAX' }
|
||||
];
|
||||
|
||||
const customFormatTokens = [
|
||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' }
|
||||
];
|
||||
|
||||
const originalTokens = [
|
||||
{ token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' },
|
||||
{ token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' }
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -297,7 +299,7 @@ class NamingModal extends Component {
|
||||
<FieldSet legend={translate('MediaInfo')}>
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
mediaInfoTokens.map(({ token, example }) => {
|
||||
mediaInfoTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
@@ -305,6 +307,7 @@ class NamingModal extends Component {
|
||||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
@@ -314,6 +317,14 @@ class NamingModal extends Component {
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<div>
|
||||
MediaInfo Full/AudioLanguages/SubtitleLanguages support a <code>:EN+DE</code> suffix allowing you to filter the languages included in the filename. Use <code>-DE</code> to exclude specific languages.
|
||||
Appending <code>+</code> (eg <code>:EN+</code>) will output <code>[EN]</code>/<code>[EN+--]</code>/<code>[--]</code> depending on excluded languages. For example <code>{'{'}MediaInfo Full:EN+DE{'}'}</code>.
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('ReleaseGroup')}>
|
||||
|
||||
@@ -35,9 +35,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
background-color: #ddd;
|
||||
|
||||
.footNote {
|
||||
padding: 2px;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.lower {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { icons, sizes } from 'Helpers/Props';
|
||||
import styles from './NamingOption.css';
|
||||
|
||||
class NamingOption extends Component {
|
||||
@@ -39,6 +40,7 @@ class NamingOption extends Component {
|
||||
token,
|
||||
tokenSeparator,
|
||||
example,
|
||||
footNote,
|
||||
tokenCase,
|
||||
isFullFilename,
|
||||
size
|
||||
@@ -60,6 +62,11 @@ class NamingOption extends Component {
|
||||
|
||||
<div className={styles.example}>
|
||||
{example.replace(/ /g, tokenSeparator)}
|
||||
|
||||
{
|
||||
footNote !== 0 &&
|
||||
<Icon className={styles.footNote} name={icons.FOOTNOTE} />
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
@@ -69,6 +76,7 @@ class NamingOption extends Component {
|
||||
NamingOption.propTypes = {
|
||||
token: PropTypes.string.isRequired,
|
||||
example: PropTypes.string.isRequired,
|
||||
footNote: PropTypes.number.isRequired,
|
||||
tokenSeparator: PropTypes.string.isRequired,
|
||||
tokenCase: PropTypes.string.isRequired,
|
||||
isFullFilename: PropTypes.bool.isRequired,
|
||||
@@ -77,6 +85,7 @@ NamingOption.propTypes = {
|
||||
};
|
||||
|
||||
NamingOption.defaultProps = {
|
||||
footNote: 0,
|
||||
size: sizes.SMALL,
|
||||
isFullFilename: false
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ class DelayProfile extends Component {
|
||||
connectDragSource
|
||||
} = this.props;
|
||||
|
||||
let preferred = titleCase(preferredProtocol);
|
||||
let preferred = `Prefer ${titleCase(preferredProtocol)}`;
|
||||
|
||||
if (!enableUsenet) {
|
||||
preferred = translate('OnlyTorrent');
|
||||
|
||||
@@ -83,7 +83,7 @@ class DelayProfiles extends Component {
|
||||
<div>
|
||||
<div className={styles.delayProfilesHeader}>
|
||||
<div className={styles.column}>
|
||||
{translate('Protocol')}
|
||||
{translate('PreferredProtocol')}
|
||||
</div>
|
||||
<div className={styles.column}>
|
||||
{translate('UsenetDelay')}
|
||||
|
||||
@@ -17,6 +17,13 @@ import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/P
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDelayProfileModalContent.css';
|
||||
|
||||
const protocolOptions = [
|
||||
{ key: 'preferUsenet', value: translate('PreferUsenet') },
|
||||
{ key: 'preferTorrent', value: translate('PreferTorrent') },
|
||||
{ key: 'onlyUsenet', value: translate('OnlyUsenet') },
|
||||
{ key: 'onlyTorrent', value: translate('OnlyTorrent') }
|
||||
];
|
||||
|
||||
function EditDelayProfileModalContent(props) {
|
||||
const {
|
||||
id,
|
||||
@@ -26,7 +33,6 @@ function EditDelayProfileModalContent(props) {
|
||||
saveError,
|
||||
item,
|
||||
protocol,
|
||||
protocolOptions,
|
||||
onInputChange,
|
||||
onProtocolChange,
|
||||
onSavePress,
|
||||
@@ -52,22 +58,24 @@ function EditDelayProfileModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
isFetching ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
!isFetching && !!error ?
|
||||
<div>
|
||||
{translate('UnableToAddANewQualityProfilePleaseTryAgain')}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
!isFetching && !error ?
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Protocol')}</FormLabel>
|
||||
<FormLabel>{translate('PreferredProtocol')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -143,19 +151,21 @@ function EditDelayProfileModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</Form>
|
||||
</Form> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id && id > 1 &&
|
||||
id && id > 1 ?
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteDelayProfilePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</Button> :
|
||||
null
|
||||
}
|
||||
|
||||
<Button
|
||||
@@ -193,7 +203,6 @@ EditDelayProfileModalContent.propTypes = {
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(delayProfileShape).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
protocolOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onProtocolChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDelayProfile, setDelayProfileValue } from 'Store/Actions/settingsActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditDelayProfileModalContent from './EditDelayProfileModalContent';
|
||||
|
||||
const newDelayProfile = {
|
||||
@@ -17,13 +16,6 @@ const newDelayProfile = {
|
||||
tags: []
|
||||
};
|
||||
|
||||
const protocolOptions = [
|
||||
{ key: 'preferUsenet', value: translate('PreferUsenet') },
|
||||
{ key: 'preferTorrent', value: translate('PreferTorrent') },
|
||||
{ key: 'onlyUsenet', value: translate('OnlyUsenet') },
|
||||
{ key: 'onlyTorrent', value: translate('OnlyTorrent') }
|
||||
];
|
||||
|
||||
function createDelayProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
@@ -79,7 +71,6 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
protocol,
|
||||
protocolOptions,
|
||||
...delayProfile
|
||||
};
|
||||
}
|
||||
|
||||
@@ -262,10 +262,10 @@ export const defaultState = {
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
optionsSelector: function(items) {
|
||||
const collectionList = items.reduce((acc, movie) => {
|
||||
if (movie.collection) {
|
||||
if (movie.collection && movie.collection.title) {
|
||||
acc.push({
|
||||
id: movie.collection.name,
|
||||
name: movie.collection.name
|
||||
id: movie.collection.title,
|
||||
name: movie.collection.title
|
||||
});
|
||||
}
|
||||
|
||||
@@ -561,7 +561,7 @@ export const actionHandlers = handleThunks({
|
||||
}, []);
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/movie/import',
|
||||
url: '/importlist/movie',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(allNewMovies)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { filterTypes, sortDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import { filterTypes, icons, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||
@@ -84,6 +86,15 @@ export const defaultState = {
|
||||
label: translate('SourceTitle'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: translate('CustomFormatScore'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: 'Custom format score'
|
||||
}),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
columnLabel: translate('Details'),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { batchActions } from 'redux-batched-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import naturalExpansion from 'Utilities/String/naturalExpansion';
|
||||
import { set, update, updateItem } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
@@ -35,7 +36,7 @@ export const defaultState = {
|
||||
relativePath: function(item, direction) {
|
||||
const relativePath = item.relativePath;
|
||||
|
||||
return relativePath.toLowerCase();
|
||||
return naturalExpansion(relativePath.toLowerCase());
|
||||
},
|
||||
|
||||
movie: function(item, direction) {
|
||||
|
||||
@@ -116,7 +116,7 @@ export const filterPredicates = {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const { collection } = item;
|
||||
|
||||
return predicate(collection ? collection.name : '', filterValue);
|
||||
return predicate(collection && collection.title ? collection.title : '', filterValue);
|
||||
},
|
||||
|
||||
originalLanguage: function(item, filterValue, type) {
|
||||
@@ -162,6 +162,14 @@ export const filterPredicates = {
|
||||
return predicate(rating, filterValue);
|
||||
},
|
||||
|
||||
rottenTomatoesRating: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
const rating = item.ratings.rottenTomatoes ? item.ratings.rottenTomatoes.value : 0;
|
||||
|
||||
return predicate(rating, filterValue);
|
||||
},
|
||||
|
||||
imdbVotes: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getNewMovie from 'Utilities/Movie/getNewMovie';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { set, update, updateItem } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
@@ -63,19 +65,81 @@ export const defaultState = {
|
||||
}
|
||||
],
|
||||
|
||||
filterPredicates: {},
|
||||
filterPredicates: {
|
||||
genres: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
let allGenres = [];
|
||||
item.movies.forEach((movie) => {
|
||||
allGenres = allGenres.concat(movie.genres);
|
||||
});
|
||||
|
||||
const genres = Array.from(new Set(allGenres)).slice(0, 3);
|
||||
|
||||
return predicate(genres, filterValue);
|
||||
},
|
||||
totalMovies: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const { movies } = item;
|
||||
|
||||
const totalMovies = movies.length;
|
||||
return predicate(totalMovies, filterValue);
|
||||
}
|
||||
},
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
label: translate('Title'),
|
||||
type: filterBuilderTypes.STRING
|
||||
},
|
||||
{
|
||||
name: 'monitored',
|
||||
label: 'Monitored',
|
||||
label: translate('Monitored'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: translate('QualityProfile'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.QUALITY_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'rootFolderPath',
|
||||
label: translate('RootFolder'),
|
||||
type: filterBuilderTypes.STRING
|
||||
},
|
||||
{
|
||||
name: 'genres',
|
||||
label: translate('Genres'),
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
optionsSelector: function(items) {
|
||||
const genreList = items.reduce((acc, collection) => {
|
||||
let collectionGenres = [];
|
||||
collection.movies.forEach((movie) => {
|
||||
collectionGenres = collectionGenres.concat(movie.genres);
|
||||
});
|
||||
|
||||
const genres = Array.from(new Set(collectionGenres)).slice(0, 3);
|
||||
|
||||
genres.forEach((genre) => {
|
||||
acc.push({
|
||||
id: genre,
|
||||
name: genre
|
||||
});
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return genreList.sort(sortByName);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'totalMovies',
|
||||
label: translate('TotalMovies'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -254,27 +318,32 @@ export const actionHandlers = handleThunks({
|
||||
const {
|
||||
collectionIds,
|
||||
monitored,
|
||||
monitor
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
rootFolderPath,
|
||||
minimumAvailability
|
||||
} = payload;
|
||||
|
||||
const response = {};
|
||||
const collections = [];
|
||||
|
||||
collectionIds.forEach((id) => {
|
||||
const collectionToUpdate = { id };
|
||||
|
||||
if (payload.hasOwnProperty('monitored')) {
|
||||
collectionToUpdate.monitored = monitored;
|
||||
}
|
||||
|
||||
collections.push(collectionToUpdate);
|
||||
});
|
||||
if (payload.hasOwnProperty('monitored')) {
|
||||
response.monitored = monitored;
|
||||
}
|
||||
|
||||
if (payload.hasOwnProperty('monitor')) {
|
||||
response.monitorMovies = monitor === 'monitored';
|
||||
}
|
||||
|
||||
response.collections = collections;
|
||||
if (payload.hasOwnProperty('qualityProfileId')) {
|
||||
response.qualityProfileId = qualityProfileId;
|
||||
}
|
||||
|
||||
if (payload.hasOwnProperty('minimumAvailability')) {
|
||||
response.minimumAvailability = minimumAvailability;
|
||||
}
|
||||
|
||||
response.rootFolderPath = rootFolderPath;
|
||||
response.collectionIds = collectionIds;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
|
||||
@@ -165,11 +165,11 @@ export const actionHandlers = handleThunks({
|
||||
requestData.quality = quality;
|
||||
}
|
||||
|
||||
if (releaseGroup) {
|
||||
if (releaseGroup !== undefined) {
|
||||
requestData.releaseGroup = releaseGroup;
|
||||
}
|
||||
|
||||
if (edition) {
|
||||
if (edition !== undefined) {
|
||||
requestData.edition = edition;
|
||||
}
|
||||
|
||||
@@ -201,11 +201,11 @@ export const actionHandlers = handleThunks({
|
||||
props.quality = quality;
|
||||
}
|
||||
|
||||
if (edition) {
|
||||
if (edition !== undefined) {
|
||||
props.edition = edition;
|
||||
}
|
||||
|
||||
if (releaseGroup) {
|
||||
if (releaseGroup !== undefined) {
|
||||
props.releaseGroup = releaseGroup;
|
||||
}
|
||||
|
||||
|
||||
@@ -178,8 +178,20 @@ export const defaultState = {
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'ratings',
|
||||
label: translate('Ratings'),
|
||||
name: 'tmdbRating',
|
||||
label: translate('TmdbRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'rottenTomatoesRating',
|
||||
label: translate('RottenTomatoesRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'imdbRating',
|
||||
label: translate('ImdbRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
@@ -215,7 +227,7 @@ export const defaultState = {
|
||||
collection: function(item) {
|
||||
const { collection ={} } = item;
|
||||
|
||||
return collection.name;
|
||||
return collection.title;
|
||||
},
|
||||
|
||||
originalLanguage: function(item) {
|
||||
@@ -224,10 +236,22 @@ export const defaultState = {
|
||||
return originalLanguage.name;
|
||||
},
|
||||
|
||||
ratings: function(item) {
|
||||
imdbRating: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
return ratings.tmdb? ratings.tmdb.value : 0;
|
||||
return ratings.imdb ? ratings.imdb.value : 0;
|
||||
},
|
||||
|
||||
tmdbRating: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
return ratings.tmdb ? ratings.tmdb.value : 0;
|
||||
},
|
||||
|
||||
rottenTomatoesRating: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
return ratings.rottenTomatoes ? ratings.rottenTomatoes.value : 0;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -315,10 +339,10 @@ export const defaultState = {
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
optionsSelector: function(items) {
|
||||
const collectionList = items.reduce((acc, movie) => {
|
||||
if (movie.collection) {
|
||||
if (movie.collection && movie.collection.title) {
|
||||
acc.push({
|
||||
id: movie.collection.name,
|
||||
name: movie.collection.name
|
||||
id: movie.collection.title,
|
||||
name: movie.collection.title
|
||||
});
|
||||
}
|
||||
|
||||
@@ -413,6 +437,11 @@ export const defaultState = {
|
||||
label: translate('ImdbRating'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'rottenTomatoesRating',
|
||||
label: translate('RottenTomatoesRating'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'imdbVotes',
|
||||
label: translate('ImdbVotes'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
pageJumpBarZIndex: 10,
|
||||
modalZIndex: 1000,
|
||||
popperZIndex: 2000
|
||||
};
|
||||
|
||||
16
frontend/src/Utilities/Number/formatCustomFormatScore.js
Normal file
16
frontend/src/Utilities/Number/formatCustomFormatScore.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
function formatCustomFormatScore(input) {
|
||||
const score = Number(input);
|
||||
|
||||
if (score > 0) {
|
||||
return `+${score}`;
|
||||
}
|
||||
|
||||
if (score < 0) {
|
||||
return score;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export default formatCustomFormatScore;
|
||||
11
frontend/src/Utilities/String/naturalExpansion.js
Normal file
11
frontend/src/Utilities/String/naturalExpansion.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const regex = /\d+/g;
|
||||
|
||||
function naturalExpansion(input) {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return input.replace(regex, (n) => n.padStart(8, '0'));
|
||||
}
|
||||
|
||||
export default naturalExpansion;
|
||||
@@ -1,9 +1,11 @@
|
||||
const regex = /\b\w+/g;
|
||||
|
||||
function titleCase(input) {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return input.replace(/\b\w+/g, (match) => {
|
||||
return input.replace(regex, (match) => {
|
||||
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"@microsoft/signalr": "6.0.5",
|
||||
"@microsoft/signalr": "6.0.8",
|
||||
"@sentry/browser": "6.18.2",
|
||||
"@sentry/integrations": "6.18.2",
|
||||
"classnames": "2.3.1",
|
||||
@@ -45,7 +45,7 @@
|
||||
"jquery": "3.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.2",
|
||||
"moment": "2.29.4",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.7.2",
|
||||
@@ -62,8 +62,7 @@
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.5.0",
|
||||
"react-slick": "0.28.1",
|
||||
"slick-carousel": "1.8.1",
|
||||
"swiper": "8.3.2",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "1.4.7",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
||||
<add key="FFMpegCore" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FFMpegCore/nuget/v3/index.json" />
|
||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
||||
@@ -212,6 +212,7 @@ namespace NzbDrone.Common.Test.Http
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
|
||||
public void should_execute_get_using_brotli()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||
|
||||
// Announce URLs (passkeys) Magnet & Tracker
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||
@@ -79,14 +80,33 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
// Notifiarr
|
||||
[TestCase(@"https://xxx.yyy/api/v1/notification/radarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
|
||||
|
||||
// Discord
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
||||
|
||||
public void should_clean_message(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
cleansedMessage.Should().NotContain("mySecret");
|
||||
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
||||
cleansedMessage.Should().NotContain("01233210");
|
||||
}
|
||||
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||
public void should_keep_message(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
cleansedMessage.Should().NotContain("mySecret");
|
||||
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
||||
cleansedMessage.Should().NotContain("01233210");
|
||||
|
||||
cleansedMessage.Should().Contain("shouldkeep1");
|
||||
cleansedMessage.Should().Contain("shouldkeep2");
|
||||
cleansedMessage.Should().Contain("shouldkeep3");
|
||||
}
|
||||
|
||||
[TestCase(@"Some message (from 32.2.3.5 user agent)")]
|
||||
[TestCase(@"Auth-Invalidated ip 32.2.3.5")]
|
||||
[TestCase(@"Auth-Success ip 32.2.3.5")]
|
||||
|
||||
@@ -18,9 +18,26 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
private static LogLevel[] SentryLevels = LogLevel.AllLevels.Where(x => x >= LogLevel.Error).ToArray();
|
||||
private static LogLevel[] OtherLevels = AllLevels.Except(SentryLevels).ToArray();
|
||||
|
||||
// TODO: SQLiteException filtering tests don't work on linux-86 and alpine customer Azure agents due to sqlite library not being loaded up, pass local
|
||||
private static Exception[] FilteredExceptions = new Exception[]
|
||||
{
|
||||
new UnauthorizedAccessException()
|
||||
// new SQLiteException(SQLiteErrorCode.Locked, "database is locked"),
|
||||
new UnauthorizedAccessException(),
|
||||
new AggregateException(new Exception[]
|
||||
{
|
||||
new UnauthorizedAccessException(),
|
||||
new UnauthorizedAccessException()
|
||||
})
|
||||
};
|
||||
|
||||
private static Exception[] NonFilteredExceptions = new Exception[]
|
||||
{
|
||||
// new SQLiteException(SQLiteErrorCode.Error, "it's borked"),
|
||||
new AggregateException(new Exception[]
|
||||
{
|
||||
new UnauthorizedAccessException(),
|
||||
new NotImplementedException()
|
||||
})
|
||||
};
|
||||
|
||||
[SetUp]
|
||||
@@ -63,6 +80,14 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
_subject.IsSentryMessage(log).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource("NonFilteredExceptions")]
|
||||
public void should_not_filter_event_for_filtered_exception_types(Exception ex)
|
||||
{
|
||||
var log = GivenLogEvent(LogLevel.Error, ex, "test");
|
||||
_subject.IsSentryMessage(log).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource("FilteredExceptions")]
|
||||
public void should_not_filter_event_for_filtered_exception_types_if_filtering_disabled(Exception ex)
|
||||
|
||||
@@ -278,7 +278,7 @@ namespace NzbDrone.Common.Test
|
||||
[Test]
|
||||
public void GetUpdateClientExePath()
|
||||
{
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath(PlatformType.DotNet).Should().BeEquivalentTo(@"C:\Temp\radarr_update\Radarr.Update.exe".AsOsAgnostic());
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\radarr_update\Radarr.Update".AsOsAgnostic().ProcessNameToExe());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -264,6 +264,11 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
protected virtual void MoveFileInternal(string source, string destination)
|
||||
{
|
||||
if (File.Exists(destination))
|
||||
{
|
||||
throw new FileAlreadyExistsException("File already exists", destination);
|
||||
}
|
||||
|
||||
File.Move(source, destination);
|
||||
}
|
||||
|
||||
|
||||
15
src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs
Normal file
15
src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
{
|
||||
public class FileAlreadyExistsException : Exception
|
||||
{
|
||||
public string Filename { get; set; }
|
||||
|
||||
public FileAlreadyExistsException(string message, string filename)
|
||||
: base(message)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,6 @@ using System;
|
||||
|
||||
namespace NzbDrone.Common.EnvironmentInfo
|
||||
{
|
||||
public enum PlatformType
|
||||
{
|
||||
DotNet = 0,
|
||||
Mono = 1,
|
||||
NetCore = 2
|
||||
}
|
||||
|
||||
public interface IPlatformInfo
|
||||
{
|
||||
Version Version { get; }
|
||||
@@ -16,31 +9,18 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
|
||||
public class PlatformInfo : IPlatformInfo
|
||||
{
|
||||
private static PlatformType _platform;
|
||||
private static Version _version;
|
||||
|
||||
static PlatformInfo()
|
||||
{
|
||||
_platform = PlatformType.NetCore;
|
||||
_version = Environment.Version;
|
||||
}
|
||||
|
||||
public static PlatformType Platform => _platform;
|
||||
public static bool IsDotNet => Platform == PlatformType.DotNet;
|
||||
public static bool IsNetCore => Platform == PlatformType.NetCore;
|
||||
|
||||
public static string PlatformName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDotNet)
|
||||
{
|
||||
return ".NET";
|
||||
}
|
||||
else
|
||||
{
|
||||
return ".NET Core";
|
||||
}
|
||||
return ".NET";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -267,9 +267,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return substring.Substring(0, lastSeparatorIndex);
|
||||
}
|
||||
|
||||
public static string ProcessNameToExe(this string processName, PlatformType runtime)
|
||||
public static string ProcessNameToExe(this string processName)
|
||||
{
|
||||
if (OsInfo.IsWindows || runtime != PlatformType.NetCore)
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
processName += ".exe";
|
||||
}
|
||||
@@ -277,11 +277,6 @@ namespace NzbDrone.Common.Extensions
|
||||
return processName;
|
||||
}
|
||||
|
||||
public static string ProcessNameToExe(this string processName)
|
||||
{
|
||||
return processName.ProcessNameToExe(PlatformInfo.Platform);
|
||||
}
|
||||
|
||||
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return appFolderInfo.AppDataFolder;
|
||||
@@ -352,9 +347,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
|
||||
}
|
||||
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo, PlatformType runtime)
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe(runtime);
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe();
|
||||
}
|
||||
|
||||
public static string GetDatabase(this IAppFolderInfo appFolderInfo)
|
||||
|
||||
@@ -55,7 +55,8 @@ namespace NzbDrone.Common.Http
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
||||
StatusCode == HttpStatusCode.RedirectMethod ||
|
||||
StatusCode == HttpStatusCode.SeeOther;
|
||||
StatusCode == HttpStatusCode.SeeOther ||
|
||||
StatusCode == HttpStatusCode.PermanentRedirect;
|
||||
|
||||
public string[] GetCookieHeaders()
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
@@ -50,7 +50,10 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Notifiarr
|
||||
new Regex(@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
new Regex(@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Discord
|
||||
new Regex(@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
|
||||
@@ -8,47 +10,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
||||
|
||||
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return logBuilder.Property("Sentry", fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
|
||||
}
|
||||
|
||||
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
{
|
||||
SentryLogger.Log(level)
|
||||
.CopyLogEvent(logBuilder.LogEventInfo)
|
||||
SentryLogger.ForLogEvent(level)
|
||||
.CopyLogEvent(logBuilder.LogEvent)
|
||||
.SentryFingerprint(fingerprint)
|
||||
.Write();
|
||||
.Log();
|
||||
|
||||
return logBuilder.Property("Sentry", null);
|
||||
return logBuilder.Property<string>("Sentry", null);
|
||||
}
|
||||
|
||||
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
|
||||
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
|
||||
{
|
||||
return logBuilder.LoggerName(logEvent.LoggerName)
|
||||
.TimeStamp(logEvent.TimeStamp)
|
||||
return logBuilder.TimeStamp(logEvent.TimeStamp)
|
||||
.Message(logEvent.Message, logEvent.Parameters)
|
||||
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
|
||||
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
|
||||
.Exception(logEvent.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class NzbDroneFileTarget : FileTarget
|
||||
{
|
||||
protected override string GetFormattedMessage(LogEventInfo logEvent)
|
||||
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||
{
|
||||
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
target.Append(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation
|
||||
|
||||
var appFolderInfo = new AppFolderInfo(startupContext);
|
||||
|
||||
RegisterGlobalFilters();
|
||||
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
RegisterDebugger();
|
||||
@@ -97,10 +99,21 @@ namespace NzbDrone.Common.Instrumentation
|
||||
target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
|
||||
|
||||
var loggingRule = new LoggingRule("*", LogLevel.Trace, target);
|
||||
|
||||
LogManager.Configuration.AddTarget("debugger", target);
|
||||
LogManager.Configuration.LoggingRules.Add(loggingRule);
|
||||
}
|
||||
|
||||
private static void RegisterGlobalFilters()
|
||||
{
|
||||
LogManager.Setup().LoadConfiguration(c =>
|
||||
{
|
||||
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
|
||||
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
|
||||
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
|
||||
});
|
||||
}
|
||||
|
||||
private static void RegisterConsole()
|
||||
{
|
||||
var level = LogLevel.Trace;
|
||||
|
||||
@@ -229,21 +229,48 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
if (FilterEvents)
|
||||
{
|
||||
var sqlEx = logEvent.Exception as SQLiteException;
|
||||
if (sqlEx != null && FilteredSQLiteErrors.Contains(sqlEx.ResultCode))
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
var aggEx = logEvent.Exception as AggregateException;
|
||||
|
||||
if (aggEx != null && aggEx.InnerExceptions.Count > 0)
|
||||
{
|
||||
return false;
|
||||
exceptions.AddRange(aggEx.InnerExceptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
exceptions.Add(logEvent.Exception);
|
||||
}
|
||||
|
||||
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
|
||||
// If any are sentry then send to sentry
|
||||
foreach (var ex in exceptions)
|
||||
{
|
||||
return false;
|
||||
var isSentry = true;
|
||||
|
||||
var sqlEx = ex as SQLiteException;
|
||||
if (sqlEx != null && FilteredSQLiteErrors.Contains(sqlEx.ResultCode))
|
||||
{
|
||||
isSentry = false;
|
||||
}
|
||||
|
||||
if (FilteredExceptionTypeNames.Contains(ex.GetType().Name))
|
||||
{
|
||||
isSentry = false;
|
||||
}
|
||||
|
||||
if (FilteredExceptionMessages.Any(x => ex.Message.Contains(x)))
|
||||
{
|
||||
isSentry = false;
|
||||
}
|
||||
|
||||
if (isSentry)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (FilteredExceptionMessages.Any(x => logEvent.Exception.Message.Contains(x)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// The exception or aggregate exception children were not sentry exceptions
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="Sentry" Version="3.15.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.0.1" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="Sentry" Version="3.20.1" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.4" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.5" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
|
||||
@@ -27,6 +27,20 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
Mocker.Resolve<IDatabase>().Vacuum();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_should_not_contain_timestamp_without_timezone_columns()
|
||||
{
|
||||
if (Db.DatabaseType != DatabaseType.PostgreSQL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mocker.Resolve<IDatabase>()
|
||||
.OpenConnection().Query("SELECT table_name, column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'public' AND data_type = 'timestamp without time zone'")
|
||||
.Should()
|
||||
.BeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void get_version()
|
||||
{
|
||||
|
||||
@@ -56,6 +56,51 @@ namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
movies.First().CollectionTmdbId.Should().Be(collections.First().TmdbId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_collection_from_movie_without_name()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Movies").Row(new
|
||||
{
|
||||
Monitored = true,
|
||||
MinimumAvailability = 4,
|
||||
ProfileId = 1,
|
||||
MovieFileId = 0,
|
||||
MovieMetadataId = 1,
|
||||
Path = string.Format("/Movies/{0}", "Title"),
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("MovieMetadata").Row(new
|
||||
{
|
||||
Title = "Title",
|
||||
CleanTitle = "CleanTitle",
|
||||
Status = 3,
|
||||
Images = new[] { new { CoverType = "Poster" } }.ToJson(),
|
||||
Recommendations = new[] { 1 }.ToJson(),
|
||||
Runtime = 90,
|
||||
OriginalTitle = "Title",
|
||||
CleanOriginalTitle = "CleanTitle",
|
||||
OriginalLanguage = 1,
|
||||
TmdbId = 132456,
|
||||
Collection = new { TmdbId = 11 }.ToJson(),
|
||||
LastInfoSync = DateTime.UtcNow,
|
||||
});
|
||||
});
|
||||
|
||||
var collections = db.Query<Collection208>("SELECT \"Id\", \"Title\", \"TmdbId\", \"Monitored\" FROM \"Collections\"");
|
||||
|
||||
collections.Should().HaveCount(1);
|
||||
collections.First().TmdbId.Should().Be(11);
|
||||
collections.First().Title.Should().Be("Collection 11");
|
||||
collections.First().Monitored.Should().BeFalse();
|
||||
|
||||
var movies = db.Query<Movie208>("SELECT \"Id\", \"CollectionTmdbId\" FROM \"MovieMetadata\"");
|
||||
|
||||
movies.Should().HaveCount(1);
|
||||
movies.First().CollectionTmdbId.Should().Be(collections.First().TmdbId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_duplicate_collection()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using FizzWare.NBuilder;
|
||||
using System;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
@@ -168,5 +170,80 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
.Should()
|
||||
.BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_when_repack_but_auto_download_repack_is_false()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.DoNotUpgrade);
|
||||
|
||||
_parsedMovieInfo.Quality.Revision.IsRepack = true;
|
||||
_movie.MovieFileId = 1;
|
||||
_movie.MovieFile = Builder<MovieFile>.CreateNew()
|
||||
.With(e => e.Quality = new QualityModel(Quality.SDTV))
|
||||
.With(e => e.ReleaseGroup = "Radarr")
|
||||
.Build();
|
||||
|
||||
var remoteMovie = Builder<RemoteMovie>.CreateNew()
|
||||
.With(e => e.ParsedMovieInfo = _parsedMovieInfo)
|
||||
.With(e => e.Movie = _movie)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteMovie, null)
|
||||
.Accepted
|
||||
.Should()
|
||||
.BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_repack_but_auto_download_repack_is_true()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.PreferAndUpgrade);
|
||||
|
||||
_parsedMovieInfo.Quality.Revision.IsRepack = true;
|
||||
_movie.MovieFileId = 1;
|
||||
_movie.MovieFile = Builder<MovieFile>.CreateNew()
|
||||
.With(e => e.Quality = new QualityModel(Quality.SDTV))
|
||||
.With(e => e.ReleaseGroup = "Radarr")
|
||||
.Build();
|
||||
|
||||
var remoteMovie = Builder<RemoteMovie>.CreateNew()
|
||||
.With(e => e.ParsedMovieInfo = _parsedMovieInfo)
|
||||
.With(e => e.Movie = _movie)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteMovie, null)
|
||||
.Accepted
|
||||
.Should()
|
||||
.BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_repacks_are_not_preferred()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.DoNotPrefer);
|
||||
|
||||
_parsedMovieInfo.Quality.Revision.IsRepack = true;
|
||||
_movie.MovieFileId = 1;
|
||||
_movie.MovieFile = Builder<MovieFile>.CreateNew()
|
||||
.With(e => e.Quality = new QualityModel(Quality.SDTV))
|
||||
.With(e => e.ReleaseGroup = "Radarr")
|
||||
.Build();
|
||||
|
||||
var remoteMovie = Builder<RemoteMovie>.CreateNew()
|
||||
.With(e => e.ParsedMovieInfo = _parsedMovieInfo)
|
||||
.With(e => e.Movie = _movie)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteMovie, null)
|
||||
.Accepted
|
||||
.Should()
|
||||
.BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
245
src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs
Normal file
245
src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Extras;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Extras
|
||||
{
|
||||
[TestFixture]
|
||||
public class ExtraServiceFixture : CoreTest<ExtraService>
|
||||
{
|
||||
private Movie _movie;
|
||||
private MovieFile _movieFile;
|
||||
private LocalMovie _localMovie;
|
||||
|
||||
private string _movieFolder;
|
||||
private string _releaseFolder;
|
||||
|
||||
private Mock<IManageExtraFiles> _subtitleService;
|
||||
private Mock<IManageExtraFiles> _otherExtraService;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_movieFolder = @"C:\Test\Movies\Movie Title".AsOsAgnostic();
|
||||
_releaseFolder = @"C:\Test\Unsorted TV\Movie.Title.2022".AsOsAgnostic();
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(s => s.Path = _movieFolder)
|
||||
.Build();
|
||||
|
||||
_movieFile = Builder<MovieFile>.CreateNew()
|
||||
.With(f => f.Path = Path.Combine(_movie.Path, "Movie Title - 2022.mkv").AsOsAgnostic())
|
||||
.With(f => f.RelativePath = @"Movie Title - 2022.mkv".AsOsAgnostic())
|
||||
.Build();
|
||||
|
||||
_localMovie = Builder<LocalMovie>.CreateNew()
|
||||
.With(l => l.Movie = _movie)
|
||||
.With(l => l.Path = Path.Combine(_releaseFolder, "Movie.Title.2022.mkv").AsOsAgnostic())
|
||||
.Build();
|
||||
|
||||
_subtitleService = new Mock<IManageExtraFiles>();
|
||||
_subtitleService.SetupGet(s => s.Order).Returns(0);
|
||||
_subtitleService.Setup(s => s.CanImportFile(It.IsAny<LocalMovie>(), It.IsAny<MovieFile>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.Returns(false);
|
||||
_subtitleService.Setup(s => s.CanImportFile(It.IsAny<LocalMovie>(), It.IsAny<MovieFile>(), It.IsAny<string>(), ".srt", It.IsAny<bool>()))
|
||||
.Returns(true);
|
||||
|
||||
_otherExtraService = new Mock<IManageExtraFiles>();
|
||||
_otherExtraService.SetupGet(s => s.Order).Returns(1);
|
||||
_otherExtraService.Setup(s => s.CanImportFile(It.IsAny<LocalMovie>(), It.IsAny<MovieFile>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.Returns(true);
|
||||
|
||||
Mocker.SetConstant<IEnumerable<IManageExtraFiles>>(new[]
|
||||
{
|
||||
_subtitleService.Object,
|
||||
_otherExtraService.Object
|
||||
});
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.FolderExists(It.IsAny<string>()))
|
||||
.Returns(false);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetParentFolder(It.IsAny<string>()))
|
||||
.Returns((string path) => Directory.GetParent(path).FullName);
|
||||
|
||||
WithExistingFolder(_movie.Path);
|
||||
WithExistingFile(_movieFile.Path);
|
||||
WithExistingFile(_localMovie.Path);
|
||||
|
||||
Mocker.GetMock<IConfigService>().Setup(v => v.ImportExtraFiles).Returns(true);
|
||||
Mocker.GetMock<IConfigService>().Setup(v => v.ExtraFileExtensions).Returns("nfo,srt");
|
||||
}
|
||||
|
||||
private void WithExistingFolder(string path, bool exists = true)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
|
||||
if (exists && dir.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
WithExistingFolder(dir);
|
||||
}
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Setup(v => v.FolderExists(path)).Returns(exists);
|
||||
}
|
||||
|
||||
private void WithExistingFile(string path, bool exists = true, int size = 1000)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
|
||||
if (exists && dir.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
WithExistingFolder(dir);
|
||||
}
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Setup(v => v.FileExists(path)).Returns(exists);
|
||||
Mocker.GetMock<IDiskProvider>().Setup(v => v.GetFileSize(path)).Returns(size);
|
||||
}
|
||||
|
||||
private void WithExistingFiles(List<string> files)
|
||||
{
|
||||
foreach (string file in files)
|
||||
{
|
||||
WithExistingFile(file);
|
||||
}
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetFiles(_releaseFolder, It.IsAny<SearchOption>()))
|
||||
.Returns(files.ToArray());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_pass_file_if_import_disabled()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().Setup(v => v.ImportExtraFiles).Returns(false);
|
||||
|
||||
var nfofile = Path.Combine(_releaseFolder, "Movie.Title.2022.nfo").AsOsAgnostic();
|
||||
|
||||
var files = new List<string>
|
||||
{
|
||||
_localMovie.Path,
|
||||
nfofile
|
||||
};
|
||||
|
||||
WithExistingFiles(files);
|
||||
|
||||
Subject.ImportMovie(_localMovie, _movieFile, true);
|
||||
|
||||
_subtitleService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny<string>(), It.IsAny<string>(), true), Times.Never());
|
||||
_otherExtraService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny<string>(), It.IsAny<string>(), true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("Movie Title - 2022.sub")]
|
||||
[TestCase("Movie Title - 2022.ass")]
|
||||
public void should_not_pass_unwanted_file(string filePath)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().Setup(v => v.ImportExtraFiles).Returns(false);
|
||||
|
||||
var nfofile = Path.Combine(_releaseFolder, filePath).AsOsAgnostic();
|
||||
|
||||
var files = new List<string>
|
||||
{
|
||||
_localMovie.Path,
|
||||
nfofile
|
||||
};
|
||||
|
||||
WithExistingFiles(files);
|
||||
|
||||
Subject.ImportMovie(_localMovie, _movieFile, true);
|
||||
|
||||
_subtitleService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny<string>(), It.IsAny<string>(), true), Times.Never());
|
||||
_otherExtraService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny<string>(), It.IsAny<string>(), true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_pass_subtitle_file_to_subtitle_service()
|
||||
{
|
||||
var subtitleFile = Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic();
|
||||
|
||||
var files = new List<string>
|
||||
{
|
||||
_localMovie.Path,
|
||||
subtitleFile
|
||||
};
|
||||
|
||||
WithExistingFiles(files);
|
||||
|
||||
Subject.ImportMovie(_localMovie, _movieFile, true);
|
||||
|
||||
_subtitleService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List<string> { subtitleFile }, true), Times.Once());
|
||||
_otherExtraService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List<string> { subtitleFile }, true), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_pass_nfo_file_to_other_service()
|
||||
{
|
||||
var nfofile = Path.Combine(_releaseFolder, "Movie.Title.2022.nfo").AsOsAgnostic();
|
||||
|
||||
var files = new List<string>
|
||||
{
|
||||
_localMovie.Path,
|
||||
nfofile
|
||||
};
|
||||
|
||||
WithExistingFiles(files);
|
||||
|
||||
Subject.ImportMovie(_localMovie, _movieFile, true);
|
||||
|
||||
_subtitleService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List<string> { nfofile }, true), Times.Never());
|
||||
_otherExtraService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List<string> { nfofile }, true), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_subtitles_when_importing_from_job_folder()
|
||||
{
|
||||
_localMovie.FolderMovieInfo = new ParsedMovieInfo();
|
||||
|
||||
var subtitleFile = Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic();
|
||||
|
||||
var files = new List<string>
|
||||
{
|
||||
_localMovie.Path,
|
||||
subtitleFile
|
||||
};
|
||||
|
||||
WithExistingFiles(files);
|
||||
|
||||
Subject.ImportMovie(_localMovie, _movieFile, true);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_releaseFolder, SearchOption.AllDirectories), Times.Once);
|
||||
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_releaseFolder, SearchOption.TopDirectoryOnly), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_search_subtitles_when_not_importing_from_job_folder()
|
||||
{
|
||||
_localMovie.FolderMovieInfo = null;
|
||||
|
||||
var subtitleFile = Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic();
|
||||
|
||||
var files = new List<string>
|
||||
{
|
||||
_localMovie.Path,
|
||||
subtitleFile
|
||||
};
|
||||
|
||||
WithExistingFiles(files);
|
||||
|
||||
Subject.ImportMovie(_localMovie, _movieFile, true);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_releaseFolder, SearchOption.AllDirectories), Times.Never);
|
||||
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_releaseFolder, SearchOption.TopDirectoryOnly), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Others;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Extras.Others
|
||||
{
|
||||
[TestFixture]
|
||||
public class OtherExtraServiceFixture : CoreTest<OtherExtraService>
|
||||
{
|
||||
private Movie _movie;
|
||||
private MovieFile _movieFile;
|
||||
private LocalMovie _localMovie;
|
||||
|
||||
private string _movieFolder;
|
||||
private string _releaseFolder;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_movieFolder = @"C:\Test\Movies\Movie Title".AsOsAgnostic();
|
||||
_releaseFolder = @"C:\Test\Unsorted Movies\Movie.Title.2022".AsOsAgnostic();
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(s => s.Path = _movieFolder)
|
||||
.Build();
|
||||
|
||||
_movieFile = Builder<MovieFile>.CreateNew()
|
||||
.With(f => f.Path = Path.Combine(_movie.Path, "Movie Title - 2022.mkv").AsOsAgnostic())
|
||||
.With(f => f.RelativePath = @"Movie Title - 2022.mkv")
|
||||
.Build();
|
||||
|
||||
_localMovie = Builder<LocalMovie>.CreateNew()
|
||||
.With(l => l.Movie = _movie)
|
||||
.With(l => l.Path = Path.Combine(_releaseFolder, "Movie.Title.2022.mkv").AsOsAgnostic())
|
||||
.With(l => l.FileMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "Movie Title" },
|
||||
Year = 2022
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("Movie Title - 2022.nfo", "Movie Title - 2022.nfo")]
|
||||
[TestCase("Movie.Title.2022.nfo", "Movie Title - 2022.nfo")]
|
||||
[TestCase("Movie Title 2022.nfo", "Movie Title - 2022.nfo")]
|
||||
[TestCase("Movie_Title_2022.nfo", "Movie Title - 2022.nfo")]
|
||||
[TestCase(@"Movie.Title.2022\thumb.jpg", "Movie Title - 2022.jpg")]
|
||||
public void should_import_matching_file(string filePath, string expectedOutputPath)
|
||||
{
|
||||
var files = new List<string> { Path.Combine(_releaseFolder, filePath).AsOsAgnostic() };
|
||||
|
||||
var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList();
|
||||
|
||||
results.Count().Should().Be(1);
|
||||
|
||||
results[0].RelativePath.AsOsAgnostic().PathEquals(expectedOutputPath.AsOsAgnostic()).Should().Be(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_import_multiple_nfo_files()
|
||||
{
|
||||
var files = new List<string>
|
||||
{
|
||||
Path.Combine(_releaseFolder, "Movie.Title.2022.nfo").AsOsAgnostic(),
|
||||
Path.Combine(_releaseFolder, "Movie_Title_2022.nfo").AsOsAgnostic(),
|
||||
};
|
||||
|
||||
var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList();
|
||||
|
||||
results.Count().Should().Be(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Subtitles;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.MovieImport;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Extras.Subtitles
|
||||
{
|
||||
[TestFixture]
|
||||
public class SubtitleServiceFixture : CoreTest<SubtitleService>
|
||||
{
|
||||
private Movie _movie;
|
||||
private MovieFile _movieFile;
|
||||
private LocalMovie _localMovie;
|
||||
|
||||
private string _MovieFolder;
|
||||
private string _releaseFolder;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_MovieFolder = @"C:\Test\Movies\Movie Title".AsOsAgnostic();
|
||||
_releaseFolder = @"C:\Test\Unsorted Movies\Movie.Title.2022".AsOsAgnostic();
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(s => s.Path = _MovieFolder)
|
||||
.Build();
|
||||
|
||||
_movieFile = Builder<MovieFile>.CreateNew()
|
||||
.With(f => f.Path = Path.Combine(_movie.Path, "Movie Title - 2022.mkv").AsOsAgnostic())
|
||||
.With(f => f.RelativePath = @"Movie Title - 2022.mkv".AsOsAgnostic())
|
||||
.Build();
|
||||
|
||||
_localMovie = Builder<LocalMovie>.CreateNew()
|
||||
.With(l => l.Movie = _movie)
|
||||
.With(l => l.Path = Path.Combine(_releaseFolder, "Movie.Title.2022.mkv").AsOsAgnostic())
|
||||
.With(l => l.FileMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitles = new List<string> { "Movie Title" },
|
||||
Year = 2022
|
||||
})
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetParentFolder(It.IsAny<string>()))
|
||||
.Returns((string path) => Directory.GetParent(path).FullName);
|
||||
|
||||
Mocker.GetMock<IDetectSample>().Setup(s => s.IsSample(It.IsAny<MovieMetadata>(), It.IsAny<string>()))
|
||||
.Returns(DetectSampleResult.NotSample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("Movie.Title.2022.en.nfo")]
|
||||
public void should_not_import_non_subtitle_file(string filePath)
|
||||
{
|
||||
var files = new List<string> { Path.Combine(_releaseFolder, filePath).AsOsAgnostic() };
|
||||
|
||||
var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList();
|
||||
|
||||
results.Count().Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("Movie Title - 2022.srt", "Movie Title - 2022.srt")]
|
||||
[TestCase("Movie.Title.2022.en.srt", "Movie Title - 2022.en.srt")]
|
||||
[TestCase("Movie.Title.2022.english.srt", "Movie Title - 2022.en.srt")]
|
||||
[TestCase("Movie Title 2022_en_sdh_forced.srt", "Movie Title - 2022.en.sdh.forced.srt")]
|
||||
[TestCase("Movie_Title_2022 en.srt", "Movie Title - 2022.en.srt")]
|
||||
[TestCase(@"Subs\Movie.Title.2022\2_en.srt", "Movie Title - 2022.en.srt")]
|
||||
[TestCase("sub.srt", "Movie Title - 2022.srt")]
|
||||
public void should_import_matching_subtitle_file(string filePath, string expectedOutputPath)
|
||||
{
|
||||
var files = new List<string> { Path.Combine(_releaseFolder, filePath).AsOsAgnostic() };
|
||||
|
||||
var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList();
|
||||
|
||||
results.Count().Should().Be(1);
|
||||
|
||||
results[0].RelativePath.AsOsAgnostic().PathEquals(expectedOutputPath.AsOsAgnostic()).Should().Be(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_import_multiple_subtitle_files_per_language()
|
||||
{
|
||||
var files = new List<string>
|
||||
{
|
||||
Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic(),
|
||||
Path.Combine(_releaseFolder, "Movie.Title.2022.eng.srt").AsOsAgnostic(),
|
||||
Path.Combine(_releaseFolder, "Subs", "Movie_Title_2022_en_forced.srt").AsOsAgnostic(),
|
||||
Path.Combine(_releaseFolder, "Subs", "Movie.Title.2022", "2_fr.srt").AsOsAgnostic()
|
||||
};
|
||||
|
||||
var expectedOutputs = new string[]
|
||||
{
|
||||
"Movie Title - 2022.1.en.srt",
|
||||
"Movie Title - 2022.2.en.srt",
|
||||
"Movie Title - 2022.en.forced.srt",
|
||||
"Movie Title - 2022.fr.srt",
|
||||
};
|
||||
|
||||
var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList();
|
||||
|
||||
results.Count().Should().Be(expectedOutputs.Length);
|
||||
|
||||
for (int i = 0; i < expectedOutputs.Length; i++)
|
||||
{
|
||||
results[i].RelativePath.AsOsAgnostic().PathEquals(expectedOutputs[i].AsOsAgnostic()).Should().Be(true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_import_multiple_subtitle_files_per_language_with_tags()
|
||||
{
|
||||
var files = new List<string>
|
||||
{
|
||||
Path.Combine(_releaseFolder, "Movie.Title.2022.en.forced.cc.srt").AsOsAgnostic(),
|
||||
Path.Combine(_releaseFolder, "Movie.Title.2022.other.en.forced.cc.srt").AsOsAgnostic(),
|
||||
Path.Combine(_releaseFolder, "Movie.Title.2022.en.forced.sdh.srt").AsOsAgnostic(),
|
||||
Path.Combine(_releaseFolder, "Movie.Title.2022.en.forced.default.srt").AsOsAgnostic(),
|
||||
};
|
||||
|
||||
var expectedOutputs = new[]
|
||||
{
|
||||
"Movie Title - 2022.1.en.forced.cc.srt",
|
||||
"Movie Title - 2022.2.en.forced.cc.srt",
|
||||
"Movie Title - 2022.en.forced.sdh.srt",
|
||||
"Movie Title - 2022.en.forced.default.srt"
|
||||
};
|
||||
|
||||
var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList();
|
||||
|
||||
results.Count().Should().Be(expectedOutputs.Length);
|
||||
|
||||
for (int i = 0; i < expectedOutputs.Length; i++)
|
||||
{
|
||||
results[i].RelativePath.AsOsAgnostic().PathEquals(expectedOutputs[i].AsOsAgnostic()).Should().Be(true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(@"Subs\2_en.srt", "Movie Title - 2022.en.srt")]
|
||||
public void should_import_unmatching_subtitle_file_if_only_episode(string filePath, string expectedOutputPath)
|
||||
{
|
||||
var subtitleFile = Path.Combine(_releaseFolder, filePath).AsOsAgnostic();
|
||||
|
||||
var sampleFile = Path.Combine(_movie.Path, "Movie Title - 2022.sample.mkv").AsOsAgnostic();
|
||||
|
||||
var videoFiles = new string[]
|
||||
{
|
||||
_localMovie.Path,
|
||||
sampleFile
|
||||
};
|
||||
|
||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetFiles(It.IsAny<string>(), SearchOption.AllDirectories))
|
||||
.Returns(videoFiles);
|
||||
|
||||
Mocker.GetMock<IDetectSample>().Setup(s => s.IsSample(It.IsAny<MovieMetadata>(), sampleFile))
|
||||
.Returns(DetectSampleResult.Sample);
|
||||
|
||||
var results = Subject.ImportFiles(_localMovie, _movieFile, new List<string> { subtitleFile }, true).ToList();
|
||||
|
||||
results.Count().Should().Be(1);
|
||||
|
||||
results[0].RelativePath.AsOsAgnostic().PathEquals(expectedOutputPath.AsOsAgnostic()).Should().Be(true);
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
public void should_return_ok_on_movie_imported_event()
|
||||
{
|
||||
GivenFolderExists(_downloadRootPath);
|
||||
var importEvent = new MovieImportedEvent(new LocalMovie(), new MovieFile(), new List<MovieFile>(), true, new DownloadClientItem());
|
||||
var importEvent = new MovieFileImportedEvent(new LocalMovie(), new MovieFile(), new List<MovieFile>(), true, new DownloadClientItem());
|
||||
|
||||
Subject.Check(importEvent).ShouldBeOk();
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.HistoryTests
|
||||
DownloadId = "abcd"
|
||||
};
|
||||
|
||||
Subject.Handle(new MovieImportedEvent(localMovie, movieFile, new List<MovieFile>(), true, downloadClientItem));
|
||||
Subject.Handle(new MovieFileImportedEvent(localMovie, movieFile, new List<MovieFile>(), true, downloadClientItem));
|
||||
|
||||
Mocker.GetMock<IHistoryRepository>()
|
||||
.Verify(v => v.Insert(It.Is<MovieHistory>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localMovie.Path))));
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_collection_items()
|
||||
public void should_delete_orphaned_collection_with_meta_but_no_movie_items()
|
||||
{
|
||||
var collection = Builder<MovieCollection>.CreateNew()
|
||||
.With(h => h.Id = 3)
|
||||
@@ -40,6 +40,27 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
|
||||
Db.Insert(movie);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_unorphaned_collection()
|
||||
{
|
||||
var collection = Builder<MovieCollection>.CreateNew()
|
||||
.With(h => h.Id = 3)
|
||||
.With(h => h.TmdbId = 123456)
|
||||
.With(h => h.Title = "Some Credit")
|
||||
.BuildNew();
|
||||
|
||||
Db.Insert(collection);
|
||||
|
||||
var movieMeta = Builder<MovieMetadata>.CreateNew().With(m => m.CollectionTmdbId = collection.TmdbId).BuildNew();
|
||||
Db.Insert(movieMeta);
|
||||
|
||||
var movie = Builder<Movie>.CreateNew().With(m => m.MovieMetadataId = movieMeta.Id).BuildNew();
|
||||
Db.Insert(movie);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace NzbDrone.Core.Test.Languages
|
||||
new object[] { 32, Language.Ukrainian },
|
||||
new object[] { 33, Language.Persian },
|
||||
new object[] { 34, Language.Bengali },
|
||||
new object[] { 35, Language.Slovak },
|
||||
new object[] { 36, Language.Latvian },
|
||||
};
|
||||
|
||||
public static object[] ToIntCases =
|
||||
@@ -88,6 +90,8 @@ namespace NzbDrone.Core.Test.Languages
|
||||
new object[] { Language.Ukrainian, 32 },
|
||||
new object[] { Language.Persian, 33 },
|
||||
new object[] { Language.Bengali, 34 },
|
||||
new object[] { Language.Slovak, 35 },
|
||||
new object[] { Language.Latvian, 36 },
|
||||
};
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
[Test]
|
||||
public void should_return_default_if_no_info_is_known()
|
||||
{
|
||||
var result = Subject.Aggregate(_localMovie, null, false);
|
||||
var result = Subject.Aggregate(_localMovie, null);
|
||||
|
||||
result.Languages.Should().Contain(_movie.MovieMetadata.Value.OriginalLanguage);
|
||||
}
|
||||
@@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
null,
|
||||
null);
|
||||
|
||||
Subject.Aggregate(_localMovie, null, false).Languages.Should().Equal(new List<Language> { Language.French });
|
||||
Subject.Aggregate(_localMovie, null).Languages.Should().Equal(new List<Language> { Language.French });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
null,
|
||||
null);
|
||||
|
||||
var aggregation = Subject.Aggregate(_localMovie, null, false);
|
||||
var aggregation = Subject.Aggregate(_localMovie, null);
|
||||
|
||||
aggregation.Languages.Should().Equal(new List<Language> { Language.German });
|
||||
}
|
||||
@@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
new List<Language> { Language.Spanish },
|
||||
null);
|
||||
|
||||
Subject.Aggregate(_localMovie, null, false).Languages.Should().Equal(new List<Language> { Language.Spanish });
|
||||
Subject.Aggregate(_localMovie, null).Languages.Should().Equal(new List<Language> { Language.Spanish });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
new List<Language> { Language.Unknown },
|
||||
null);
|
||||
|
||||
Subject.Aggregate(_localMovie, null, false).Languages.Should().Equal(new List<Language> { Language.French, Language.German });
|
||||
Subject.Aggregate(_localMovie, null).Languages.Should().Equal(new List<Language> { Language.French, Language.German });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -129,7 +129,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
new List<Language> { Language.Unknown },
|
||||
new List<Language> { Language.Japanese, Language.English });
|
||||
|
||||
Subject.Aggregate(_localMovie, null, false).Languages.Should().Equal(new List<Language> { Language.Japanese, Language.English });
|
||||
Subject.Aggregate(_localMovie, null).Languages.Should().Equal(new List<Language> { Language.Japanese, Language.English });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -140,7 +140,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
new List<Language> { Language.Unknown },
|
||||
new List<Language> { Language.Unknown });
|
||||
|
||||
Subject.Aggregate(_localMovie, null, false).Languages.Should().Equal(new List<Language> { Language.French, Language.German });
|
||||
Subject.Aggregate(_localMovie, null).Languages.Should().Equal(new List<Language> { Language.French, Language.German });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
|
||||
GivenAugmenters(_fileExtensionAugmenter, nullMock);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), null, false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), null);
|
||||
|
||||
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Extension);
|
||||
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.Extension);
|
||||
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
{
|
||||
GivenAugmenters(_fileExtensionAugmenter, _nameAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), null, false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), null);
|
||||
|
||||
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
{
|
||||
GivenAugmenters(_fileExtensionAugmenter, _mediaInfoAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), null, false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), null);
|
||||
|
||||
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Extension);
|
||||
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.MediaInfo);
|
||||
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
{
|
||||
GivenAugmenters(_nameAugmenter, _mediaInfoAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), null, false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), null);
|
||||
|
||||
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.MediaInfo);
|
||||
@@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
{
|
||||
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem(), false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem());
|
||||
|
||||
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
@@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
{
|
||||
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem(), false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem());
|
||||
|
||||
result.Quality.Revision.Version.Should().Be(1);
|
||||
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Unknown);
|
||||
@@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
|
||||
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem(), false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem());
|
||||
|
||||
result.Quality.Revision.Version.Should().Be(2);
|
||||
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
|
||||
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem(), false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem());
|
||||
|
||||
result.Quality.Revision.Version.Should().Be(0);
|
||||
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
|
||||
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
|
||||
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem(), false);
|
||||
var result = Subject.Aggregate(new LocalMovie(), new DownloadClientItem());
|
||||
|
||||
result.Quality.Revision.Version.Should().Be(2);
|
||||
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Name);
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
Movie = _movie
|
||||
};
|
||||
|
||||
Subject.Aggregate(localMovie, null, false);
|
||||
Subject.Aggregate(localMovie, null);
|
||||
|
||||
localMovie.ReleaseGroup.Should().Be("Viva");
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
Movie = _movie
|
||||
};
|
||||
|
||||
Subject.Aggregate(localMovie, null, false);
|
||||
Subject.Aggregate(localMovie, null);
|
||||
|
||||
localMovie.ReleaseGroup.Should().Be("Drone");
|
||||
}
|
||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
Movie = _movie
|
||||
};
|
||||
|
||||
Subject.Aggregate(localMovie, null, false);
|
||||
Subject.Aggregate(localMovie, null);
|
||||
|
||||
localMovie.ReleaseGroup.Should().Be("Wizzy");
|
||||
}
|
||||
@@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
Movie = _movie
|
||||
};
|
||||
|
||||
Subject.Aggregate(localMovie, null, false);
|
||||
Subject.Aggregate(localMovie, null);
|
||||
|
||||
localMovie.ReleaseGroup.Should().Be("FraMeSToR");
|
||||
}
|
||||
@@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
|
||||
Movie = _movie
|
||||
};
|
||||
|
||||
Subject.Aggregate(localMovie, null, false);
|
||||
Subject.Aggregate(localMovie, null);
|
||||
|
||||
localMovie.ReleaseGroup.Should().Be("FraMeSToR");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.MovieImport;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
{
|
||||
[TestFixture]
|
||||
public class GetSceneNameFixture : CoreTest
|
||||
{
|
||||
private LocalMovie _localMovie;
|
||||
private string _movieName = "movie.title.2022.dvdrip.x264-ingot";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var movie = Builder<Movie>.CreateNew()
|
||||
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
.With(s => s.Path = @"C:\Test\Movies\Movie Title".AsOsAgnostic())
|
||||
.Build();
|
||||
|
||||
_localMovie = new LocalMovie
|
||||
{
|
||||
Movie = movie,
|
||||
Path = Path.Combine(movie.Path, "Movie Title - 2022 - Episode Title.mkv"),
|
||||
Quality = new QualityModel(Quality.Bluray720p),
|
||||
ReleaseGroup = "DRONE"
|
||||
};
|
||||
}
|
||||
|
||||
private void GivenExistingFileOnDisk()
|
||||
{
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
|
||||
.Returns(new List<MovieFile>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_download_client_item_title_as_scene_name()
|
||||
{
|
||||
_localMovie.DownloadClientMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = _movieName
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.Be(_movieName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_download_client_item_title_as_scene_name_if_there_are_other_video_files()
|
||||
{
|
||||
_localMovie.OtherVideoFiles = true;
|
||||
_localMovie.DownloadClientMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = _movieName
|
||||
};
|
||||
|
||||
_localMovie.Path = Path.Combine(@"C:\Test\Unsorted Movies", _movieName)
|
||||
.AsOsAgnostic();
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_file_name_as_scenename_only_if_it_looks_like_scenename()
|
||||
{
|
||||
_localMovie.Path = Path.Combine(@"C:\Test\Unsorted Movies", _movieName + ".mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.Be(_movieName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_file_name_as_scenename_if_it_doesnt_look_like_scenename()
|
||||
{
|
||||
_localMovie.Path = Path.Combine(@"C:\Test\Unsorted Movies", _movieName, "aaaaa.mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_folder_name_as_scenename_only_if_it_looks_like_scenename()
|
||||
{
|
||||
_localMovie.FolderMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = _movieName
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.Be(_movieName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_folder_name_as_scenename_if_it_doesnt_look_like_scenename()
|
||||
{
|
||||
_localMovie.Path = Path.Combine(@"C:\Test\Unsorted Movies", _movieName, "aaaaa.mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
_localMovie.FolderMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = "aaaaa"
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_folder_name_as_scenename_if_there_are_other_video_files()
|
||||
{
|
||||
_localMovie.OtherVideoFiles = true;
|
||||
_localMovie.Path = Path.Combine(@"C:\Test\Unsorted Movies", _movieName, "aaaaa.mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
_localMovie.FolderMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = _movieName
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[TestCase(".mkv")]
|
||||
[TestCase(".par2")]
|
||||
[TestCase(".nzb")]
|
||||
public void should_remove_extension_from_nzb_title_for_scene_name(string extension)
|
||||
{
|
||||
_localMovie.DownloadClientMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = _movieName + extension
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localMovie).Should()
|
||||
.Be(_movieName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<MovieImportedEvent>()), Times.Once());
|
||||
.Verify(v => v.PublishEvent(It.IsAny<MovieFileImportedEvent>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -159,84 +159,6 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_nzb_title_as_scene_name()
|
||||
{
|
||||
GivenNewDownload();
|
||||
_downloadClientItem.Title = "malcolm.in.the.middle.2015.dvdrip.xvid-ingot";
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<MovieFile>(c => c.SceneName == _downloadClientItem.Title)));
|
||||
}
|
||||
|
||||
[TestCase(".mkv")]
|
||||
[TestCase(".par2")]
|
||||
[TestCase(".nzb")]
|
||||
public void should_remove_extension_from_nzb_title_for_scene_name(string extension)
|
||||
{
|
||||
GivenNewDownload();
|
||||
var title = "malcolm.in.the.middle.2015.dvdrip.xvid-ingot";
|
||||
|
||||
_downloadClientItem.Title = title + extension;
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<MovieFile>(c => c.SceneName == title)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_file_name_as_scenename_only_if_it_looks_like_scenename()
|
||||
{
|
||||
GivenNewDownload();
|
||||
_approvedDecisions.First().LocalMovie.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "movie.title.2018.dvdrip.xvid-ingot.mkv");
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<MovieFile>(c => c.SceneName == "movie.title.2018.dvdrip.xvid-ingot")));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_file_name_as_scenename_if_it_doesnt_looks_like_scenename()
|
||||
{
|
||||
GivenNewDownload();
|
||||
_approvedDecisions.First().LocalMovie.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<MovieFile>(c => c.SceneName == null)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_folder_name_as_scenename_only_if_it_looks_like_scenename()
|
||||
{
|
||||
GivenNewDownload();
|
||||
_approvedDecisions.First().LocalMovie.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
|
||||
_approvedDecisions.First().LocalMovie.FolderMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = "movie.title.2018.dvdrip.xvid-ingot"
|
||||
};
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<MovieFile>(c => c.SceneName == "movie.title.2018.dvdrip.xvid-ingot")));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_folder_name_as_scenename_if_it_doesnt_looks_like_scenename()
|
||||
{
|
||||
GivenNewDownload();
|
||||
_approvedDecisions.First().LocalMovie.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
|
||||
_approvedDecisions.First().LocalMovie.FolderMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
ReleaseTitle = "aaaaa.mkv"
|
||||
};
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<MovieFile>(c => c.SceneName == null)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_import_larger_files_first()
|
||||
{
|
||||
@@ -411,5 +333,18 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<MovieFile>(c => c.OriginalFilePath == $"subfolder\\{name}.mkv".AsOsAgnostic())));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_include_scene_name_with_new_downloads()
|
||||
{
|
||||
var firstDecision = _approvedDecisions.First();
|
||||
firstDecision.LocalMovie.SceneName = "Movie.Title.2022.dvdrip-DRONE";
|
||||
|
||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||
|
||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||
.Verify(v => v.UpgradeMovieFile(It.Is<MovieFile>(e => e.SceneName == firstDecision.LocalMovie.SceneName), _approvedDecisions.First().LocalMovie, false),
|
||||
Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
private void GivenAugmentationSuccess()
|
||||
{
|
||||
Mocker.GetMock<IAggregationService>()
|
||||
.Setup(s => s.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()))
|
||||
.Callback<LocalMovie, DownloadClientItem, bool>((localMovie, downloadClientItem, otherFiles) =>
|
||||
.Setup(s => s.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Callback<LocalMovie, DownloadClientItem>((localMovie, downloadClientItem) =>
|
||||
{
|
||||
localMovie.Movie = _localMovie.Movie;
|
||||
});
|
||||
@@ -173,7 +173,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
GivenSpecifications(_pass1);
|
||||
|
||||
Mocker.GetMock<IAggregationService>()
|
||||
.Setup(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()))
|
||||
.Setup(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Throws<TestException>();
|
||||
|
||||
_videoFiles = new List<string>
|
||||
@@ -188,7 +188,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
Subject.GetImportDecisions(_videoFiles, _movie);
|
||||
|
||||
Mocker.GetMock<IAggregationService>()
|
||||
.Verify(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
|
||||
.Verify(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>()), Times.Exactly(_videoFiles.Count));
|
||||
|
||||
ExceptionVerification.ExpectedErrors(3);
|
||||
}
|
||||
@@ -209,7 +209,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
var fileNames = _videoFiles.Select(System.IO.Path.GetFileName);
|
||||
|
||||
Mocker.GetMock<IAggregationService>()
|
||||
.Setup(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()))
|
||||
.Setup(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Throws<TestException>();
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
|
||||
public void should_return_a_decision_when_exception_is_caught()
|
||||
{
|
||||
Mocker.GetMock<IAggregationService>()
|
||||
.Setup(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()))
|
||||
.Setup(c => c.Augment(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Throws<TestException>();
|
||||
|
||||
_videoFiles = new List<string>
|
||||
|
||||
@@ -11,6 +11,7 @@ using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Collections;
|
||||
using NzbDrone.Core.Movies.Commands;
|
||||
using NzbDrone.Core.Movies.Credits;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
@@ -52,6 +53,10 @@ namespace NzbDrone.Core.Test.MovieTests
|
||||
Mocker.GetMock<IProvideMovieInfo>()
|
||||
.Setup(s => s.GetMovieInfo(It.IsAny<int>()))
|
||||
.Callback<int>((i) => { throw new MovieNotFoundException(i); });
|
||||
|
||||
Mocker.GetMock<IRootFolderService>()
|
||||
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
|
||||
.Returns(string.Empty);
|
||||
}
|
||||
|
||||
private void GivenNewMovieInfo(MovieMetadata movie)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class EditionTagsFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Movie _movie;
|
||||
private MovieFile _movieFile;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_movie = Builder<Movie>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "South Park")
|
||||
.Build();
|
||||
|
||||
_movieFile = new MovieFile { Quality = new QualityModel(), ReleaseGroup = "SonarrTest" };
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.RenameMovies = true;
|
||||
|
||||
Mocker.GetMock<INamingConfigService>()
|
||||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_edition_tag()
|
||||
{
|
||||
_movieFile.Edition = "Uncut";
|
||||
_namingConfig.StandardMovieFormat = "{Movie Title} [{Edition Tags}]";
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
.Should().Be("South Park [Uncut]");
|
||||
}
|
||||
|
||||
[TestCase("{Movie Title} {edition-{Edition Tags}}")]
|
||||
public void should_conditional_hide_edition_tags_in_plex_format(string movieFormat)
|
||||
{
|
||||
_movieFile.Edition = "";
|
||||
_namingConfig.StandardMovieFormat = movieFormat;
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
.Should().Be("South Park");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,7 @@ using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[Platform(Exclude = "Win")]
|
||||
[TestFixture]
|
||||
|
||||
public class FileNameBuilderFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Movie _movie;
|
||||
@@ -427,7 +425,6 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
.Should().Be("South.Park.H264.DTS.[EN+ES+IT]");
|
||||
}
|
||||
|
||||
[Ignore("not currently supported")]
|
||||
[Test]
|
||||
public void should_format_mediainfo_3d_properly()
|
||||
{
|
||||
@@ -671,6 +668,27 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("eng/deu", "", "[EN+DE]")]
|
||||
[TestCase("eng/nld/deu", "", "[EN+NL+DE]")]
|
||||
[TestCase("eng/deu", ":DE", "[DE]")]
|
||||
[TestCase("eng/nld/deu", ":EN+NL", "[EN+NL]")]
|
||||
[TestCase("eng/nld/deu", ":NL+EN", "[NL+EN]")]
|
||||
[TestCase("eng/nld/deu", ":-NL", "[EN+DE]")]
|
||||
[TestCase("eng/nld/deu", ":DE+", "[DE+-]")]
|
||||
[TestCase("eng/nld/deu", ":DE+NO.", "[DE].")]
|
||||
[TestCase("eng/nld/deu", ":-EN-", "[NL+DE]-")]
|
||||
public void should_format_subtitle_languages_all(string subtitleLanguages, string format, string expected)
|
||||
{
|
||||
_movieFile.ReleaseGroup = null;
|
||||
|
||||
GivenMediaInfoModel(subtitles: subtitleLanguages);
|
||||
|
||||
_namingConfig.StandardMovieFormat = "{MediaInfo SubtitleLanguages" + format + "}End";
|
||||
|
||||
Subject.BuildFileName(_movie, _movieFile)
|
||||
.Should().Be(expected + "End");
|
||||
}
|
||||
|
||||
[TestCase(HdrFormat.None, "South.Park")]
|
||||
[TestCase(HdrFormat.Hlg10, "South.Park.HDR")]
|
||||
[TestCase(HdrFormat.Hdr10, "South.Park.HDR")]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Internal;
|
||||
@@ -47,5 +47,25 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
Subject.GetMovieFolder(_movie)
|
||||
.Should().Be($"Movie Title ({_movie.TmdbId})");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_imdb_tag()
|
||||
{
|
||||
_namingConfig.MovieFolderFormat = "{Movie Title} {imdb-{ImdbId}}";
|
||||
|
||||
Subject.GetMovieFolder(_movie)
|
||||
.Should().Be($"Movie Title {{imdb-{_movie.ImdbId}}}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_imdb_tag_if_null()
|
||||
{
|
||||
_namingConfig.MovieFolderFormat = "{Movie Title} {imdb-{ImdbId}}";
|
||||
|
||||
_movie.ImdbId = null;
|
||||
|
||||
Subject.GetMovieFolder(_movie)
|
||||
.Should().Be($"Movie Title");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
src/NzbDrone.Core.Test/ParserTests/AnimeVersionFixture.cs
Normal file
23
src/NzbDrone.Core.Test/ParserTests/AnimeVersionFixture.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class AnimeVersionFixture : CoreTest
|
||||
{
|
||||
[TestCase("Anime Title - 2018 - (BD 1080p HEVC FLAC) [Dual Audio] [Group]", 1)]
|
||||
[TestCase("Anime Title - 2018v2 - (BD 1080p HEVC FLAC) [Dual Audio] [Group]", 2)]
|
||||
[TestCase("Anime Title - 2018 v2 - (BD 1080p HEVC FLAC) [Dual Audio] [Group]", 2)]
|
||||
[TestCase("[SubsPlease] Anime Title - 01 (1080p) [B1F227CF]", 1)]
|
||||
[TestCase("[SubsPlease] Anime Title - 01v2 (1080p) [B1F227CF]", 2)]
|
||||
[TestCase("[SubsPlease] Anime Title - 01 v2 (1080p) [B1F227CF]", 2)]
|
||||
public void should_be_able_to_parse_repack(string title, int version)
|
||||
{
|
||||
var result = QualityParser.ParseQuality(title);
|
||||
result.Revision.Version.Should().Be(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,22 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
result.Languages.Should().BeEquivalentTo(Language.Unknown);
|
||||
}
|
||||
|
||||
[TestCase("Movie Title - 2022.en.sub")]
|
||||
[TestCase("Movie Title - 2022.EN.sub")]
|
||||
[TestCase("Movie Title - 2022.eng.sub")]
|
||||
[TestCase("Movie Title - 2022.ENG.sub")]
|
||||
[TestCase("Movie Title - 2022.English.sub")]
|
||||
[TestCase("Movie Title - 2022.english.sub")]
|
||||
[TestCase("Movie Title - 2022.en.cc.sub")]
|
||||
[TestCase("Movie Title - 2022.en.sdh.sub")]
|
||||
[TestCase("Movie Title - 2022.en.forced.sub")]
|
||||
[TestCase("Movie Title - 2022.en.sdh.forced.sub")]
|
||||
public void should_parse_subtitle_language_english(string fileName)
|
||||
{
|
||||
var result = LanguageParser.ParseSubtitleLanguage(fileName);
|
||||
result.Should().Be(Language.English);
|
||||
}
|
||||
|
||||
[TestCase("Movie.Title.1994.French.1080p.XviD-LOL")]
|
||||
[TestCase("Movie Title : Other Title 2011 AVC.1080p.Blu-ray HD.VOSTFR.VFF")]
|
||||
[TestCase("Movie Title - Other Title 2011 Bluray 4k HDR HEVC AC3 VFF")]
|
||||
@@ -55,6 +71,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Movie Title (2020)[BDRemux AVC 1080p][E-AC3 DD Plus 5.1 Castellano-Inglés Subs]")]
|
||||
[TestCase("Movie Title (2020) [UHDRemux2160p HDR][DTS-HD MA 5.1 AC3 5.1 Castellano - True-HD 7.1 Atmos Inglés Subs]")]
|
||||
[TestCase("Movie Title (2016) [UHDRemux 2160p SDR] [Castellano DD 5.1 - Inglés DTS-HD MA 5.1 Subs]")]
|
||||
[TestCase("Movie Title 2022 [HDTV 720p][Cap.101][AC3 5.1 Castellano][www.pctnew.ORG]")]
|
||||
[TestCase("Movie Title 2022 [HDTV 720p][Cap.206][AC3 5.1 Español Castellano]")]
|
||||
public void should_parse_language_spanish(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||
@@ -342,6 +360,26 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
result.Languages.Should().BeEquivalentTo(Language.Bengali);
|
||||
}
|
||||
|
||||
[TestCase("Movie.Title.1994.HDTV.x264.SK-iCZi")]
|
||||
[TestCase("Movie.Title.2019.1080p.HDTV.x265.iNTERNAL.SK-iCZi")]
|
||||
[TestCase("Movie.Title.2018.SLOVAK.DUAL.2160p.UHD.BluRay.x265-iCZi")]
|
||||
[TestCase("Movie.Title.1990.SLOVAK.HDTV.x264-iCZi")]
|
||||
public void should_parse_language_slovak(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle);
|
||||
result.Languages.Should().BeEquivalentTo(Language.Slovak);
|
||||
}
|
||||
|
||||
[TestCase("Movie.Title.2022.LV.WEBRip.XviD-LOL")]
|
||||
[TestCase("Movie.Title.2022.lv.WEBRip.XviD-LOL")]
|
||||
[TestCase("Movie.Title.2022.LATVIAN.WEBRip.XviD-LOL")]
|
||||
[TestCase("Movie.Title.2022.Latvian.WEBRip.XviD-LOL")]
|
||||
public void should_parse_language_latvian(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle);
|
||||
result.Languages.Should().BeEquivalentTo(Language.Latvian);
|
||||
}
|
||||
|
||||
[TestCase("Movie.Title.en.sub")]
|
||||
[TestCase("Movie Title.eng.sub")]
|
||||
[TestCase("Movie.Title.eng.forced.sub")]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user