mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-12 15:30:39 -04:00
Compare commits
118 Commits
list-exclu
...
v4.3.0.667
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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
|
||||
|
||||
@@ -9,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.2.0'
|
||||
majorVersion: '4.3.0'
|
||||
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'
|
||||
@@ -549,7 +549,7 @@ stages:
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
@@ -576,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: |
|
||||
@@ -686,7 +687,7 @@ stages:
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
@@ -721,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: |
|
||||
|
||||
@@ -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]'
|
||||
}
|
||||
|
||||
@@ -181,13 +181,12 @@ class Blocklist extends Component {
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlocklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
index={index}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
|
||||
@@ -66,7 +66,8 @@ class CollectionFooter extends Component {
|
||||
monitor,
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
minimumAvailability
|
||||
minimumAvailability,
|
||||
rootFolderPath
|
||||
} = this.state;
|
||||
|
||||
const changes = {};
|
||||
@@ -87,6 +88,10 @@ class CollectionFooter extends Component {
|
||||
changes.minimumAvailability = minimumAvailability;
|
||||
}
|
||||
|
||||
if (rootFolderPath !== NO_CHANGE) {
|
||||
changes.rootFolderPath = rootFolderPath;
|
||||
}
|
||||
|
||||
this.props.onUpdateSelectedPress(changes);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,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,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}
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
|
||||
.tmdbId,
|
||||
.movieYear {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
flex: 0 0 70px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1 0 auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
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 ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
||||
import styles from './ImportListExclusion.css';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
|
||||
class ImportListExclusion extends Component {
|
||||
|
||||
@@ -58,82 +55,28 @@ class ImportListExclusion extends Component {
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
columns,
|
||||
movieTitle,
|
||||
tmdbId,
|
||||
movieYear
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.importExclusion
|
||||
)}
|
||||
>
|
||||
<div className={styles.tmdbId}>{tmdbId}</div>
|
||||
<div className={styles.movieTitle}>{movieTitle}</div>
|
||||
<div className={styles.movieYear}>{movieYear}</div>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'tmdbId') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{tmdbId}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{movieTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieYear') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{movieYear}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.EDIT}
|
||||
onPress={this.onEditImportExclusionPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteImportExclusionPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
<div className={styles.actions}>
|
||||
<Link
|
||||
onPress={this.onEditImportExclusionPress}
|
||||
>
|
||||
<Icon name={icons.EDIT} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<EditImportListExclusionModalConnector
|
||||
id={id}
|
||||
@@ -151,7 +94,7 @@ class ImportListExclusion extends Component {
|
||||
onConfirm={this.onConfirmDeleteImportExclusion}
|
||||
onCancel={this.onDeleteImportExclusionModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -161,9 +104,6 @@ ImportListExclusion.propTypes = {
|
||||
movieTitle: PropTypes.string.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
movieYear: PropTypes.number.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onConfirmDeleteImportExclusion: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -4,12 +4,8 @@ import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
||||
import ImportListExclusion from './ImportListExclusion';
|
||||
import styles from './ImportListExclusions.css';
|
||||
@@ -23,10 +19,6 @@ class ImportListExclusions extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isAddImportExclusionModalOpen: false
|
||||
};
|
||||
}
|
||||
@@ -34,16 +26,6 @@ class ImportListExclusions extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onAddImportExclusionPress = () => {
|
||||
this.setState({ isAddImportExclusionModalOpen: true });
|
||||
};
|
||||
@@ -59,50 +41,41 @@ class ImportListExclusions extends Component {
|
||||
const {
|
||||
items,
|
||||
onConfirmDeleteImportExclusion,
|
||||
columns,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('ListExclusions')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('UnableToLoadListExclusions')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.importListExclusionsHeader}>
|
||||
<div className={styles.tmdbId}>
|
||||
TMDb Id
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{translate('Title')}
|
||||
</div>
|
||||
<div className={styles.movieYear}>
|
||||
{translate('Year')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<ImportListExclusion
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
{...item}
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
index={index}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onConfirmDeleteImportExclusion={onConfirmDeleteImportExclusion}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<ImportListExclusion
|
||||
key={item.id}
|
||||
{...item}
|
||||
{...otherProps}
|
||||
index={index}
|
||||
onConfirmDeleteImportExclusion={onConfirmDeleteImportExclusion}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.addImportExclusion}>
|
||||
@@ -128,7 +101,6 @@ class ImportListExclusions extends Component {
|
||||
ImportListExclusions.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteImportExclusion: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -166,7 +166,7 @@ function EditImportListModalContent(props) {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Folder')}</FormLabel>
|
||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
|
||||
@@ -4,7 +4,6 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
//
|
||||
// Variables
|
||||
@@ -49,34 +48,7 @@ export default {
|
||||
items: [],
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
pendingChanges: {},
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'tmdbId',
|
||||
label: 'TmdbId',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'movieTitle',
|
||||
label: translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'movieYear',
|
||||
label: translate('Year'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: translate('Actions'),
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
}
|
||||
]
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ export const defaultState = {
|
||||
collection: function(item) {
|
||||
const { collection ={} } = item;
|
||||
|
||||
return collection.name;
|
||||
return collection.title;
|
||||
},
|
||||
|
||||
originalLanguage: function(item) {
|
||||
@@ -339,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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
pageJumpBarZIndex: 10,
|
||||
modalZIndex: 1000,
|
||||
popperZIndex: 2000
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
@@ -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>
|
||||
|
||||
@@ -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""}")]
|
||||
@@ -84,9 +85,24 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="5.0.1" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="Sentry" Version="3.15.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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
@@ -342,6 +358,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")]
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("A.I.Artificial.Movie.(2001)", "A.I. Artificial Movie")]
|
||||
[TestCase("A.Movie.Name.(1998)", "A Movie Name")]
|
||||
[TestCase("www.Torrenting.com - Movie.2008.720p.X264-DIMENSION", "Movie")]
|
||||
[TestCase("www.5MovieRulz.tc - Movie (2000) Malayalam HQ HDRip - x264 - AAC - 700MB.mkv", "Movie")]
|
||||
[TestCase("Movie: The Movie World 2013", "Movie: The Movie World")]
|
||||
[TestCase("Movie.The.Final.Chapter.2016", "Movie The Final Chapter")]
|
||||
[TestCase("Der.Movie.James.German.Bluray.FuckYou.Pso.Why.cant.you.follow.scene.rules.1998", "Der Movie James")]
|
||||
|
||||
@@ -41,6 +41,14 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
ParseAndVerifyQuality(title, Source.TELESYNC, proper, Resolution.R720p);
|
||||
}
|
||||
|
||||
[TestCase("Movie Name 2018 NEW PROPER 720p HD-CAM X264 HQ-CPG", true)]
|
||||
[TestCase("Movie Name (2022) 1080p HQCAM ENG x264 AAC - QRips", false)]
|
||||
[TestCase("Movie Name (2018) 720p Hindi HQ CAMrip x264 AAC 1.4GB", false)]
|
||||
public void should_parse_cam(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Source.CAM, proper, Resolution.Unknown);
|
||||
}
|
||||
|
||||
[TestCase("S07E23 .avi ", false)]
|
||||
[TestCase("Movie Name S02E01 HDTV XviD 2HD", false)]
|
||||
[TestCase("Movie Name S05E11 PROPER HDTV XviD 2HD", true)]
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("The.Movie.Title.2013.720p.BluRay.x264-ROUGH [PublicHD]", "ROUGH")]
|
||||
[TestCase("Some.Really.Bad.Movie.Title.[2021].1080p.WEB-HDRip.Dual.Audio.[Hindi.[Clean]. .English].x264.AAC.DD.2.0.By.Full4Movies.mkv-xpost", null)]
|
||||
[TestCase("The.Movie.Title.2013.1080p.10bit.AMZN.WEB-DL.DDP5.1.HEVC-Vyndros", "Vyndros")]
|
||||
[TestCase("Movie.Name.2022.1080p.BluRay.x264-[YTS.AG]", "YTS.AG")]
|
||||
public void should_parse_expected_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
@@ -100,6 +101,11 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Yet Another Anime Movie 2012 [Kametsu] [Blu-ray][MKV][h264 10-bit][1080p][FLAC 5.1][Dual Audio][Softsubs (Kametsu)]", "Kametsu")]
|
||||
[TestCase("Another.Anime.Film.Name.2016.JPN.Blu-Ray.Remux.AVC.DTS-MA.BluDragon", "BluDragon")]
|
||||
[TestCase("A Movie in the Name (1964) (1080p BluRay x265 r00t)", "r00t")]
|
||||
[TestCase("Movie Title (2022) (2160p ATV WEB-DL Hybrid H265 DV HDR DDP Atmos 5.1 English - HONE)", "HONE")]
|
||||
[TestCase("Movie Title (2009) (2160p PMTP WEB-DL Hybrid H265 DV HDR10+ DDP Atmos 5.1 English - HONE)", "HONE")]
|
||||
[TestCase("Why.Cant.You.Use.Normal.Characters.2021.2160p.UHD.HDR10+.BluRay.TrueHD.Atmos.7.1.x265-ZØNEHD", "ZØNEHD")]
|
||||
[TestCase("Movie.Should.Not.Use.Dots.2022.1080p.BluRay.x265.10bit.Tigole", "Tigole")]
|
||||
[TestCase("Movie.Title.2005.2160p.UHD.BluRay.TrueHD 7.1.Atmos.x265 - HQMUX", "HQMUX")]
|
||||
public void should_parse_exception_release_group(string title, string expected)
|
||||
{
|
||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||
|
||||
161
src/NzbDrone.Core.Test/ParserTests/SlugParserFixture.cs
Normal file
161
src/NzbDrone.Core.Test/ParserTests/SlugParserFixture.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using FluentAssertions;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SlugParserFixture : CoreTest
|
||||
{
|
||||
[TestCase("tèst", "test")]
|
||||
[TestCase("têst", "test")]
|
||||
[TestCase("tëst", "test")]
|
||||
[TestCase("tËst", "test")]
|
||||
[TestCase("áccent", "accent")]
|
||||
[TestCase("àccent", "accent")]
|
||||
[TestCase("âccent", "accent")]
|
||||
[TestCase("Äccent", "accent")]
|
||||
[TestCase("åccent", "accent")]
|
||||
[TestCase("acceñt", "accent")]
|
||||
[TestCase("ßtest", "test")]
|
||||
[TestCase("œtest", "test")]
|
||||
[TestCase("Œtest", "test")]
|
||||
[TestCase("Øtest", "test")]
|
||||
public void should_replace_accents(string input, string result)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input).Should().Be(result);
|
||||
}
|
||||
|
||||
[TestCase("Test'Result")]
|
||||
[TestCase("Test$Result")]
|
||||
[TestCase("Test(Result")]
|
||||
[TestCase("Test)Result")]
|
||||
[TestCase("Test*Result")]
|
||||
[TestCase("Test?Result")]
|
||||
[TestCase("Test/Result")]
|
||||
[TestCase("Test=Result")]
|
||||
[TestCase("Test\\Result")]
|
||||
public void should_replace_special_characters(string input)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input).Should().Be("testresult");
|
||||
}
|
||||
|
||||
[TestCase("ThIS IS A MiXeD CaSe SensItIvE ValUe")]
|
||||
public void should_lowercase_capitals(string input)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input).Should().Be("this-is-a-mixed-case-sensitive-value");
|
||||
}
|
||||
|
||||
[TestCase("test----")]
|
||||
[TestCase("test____")]
|
||||
[TestCase("test-_--_")]
|
||||
public void should_trim_trailing_dashes_and_underscores(string input)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input).Should().Be("test");
|
||||
}
|
||||
|
||||
[TestCase("test result")]
|
||||
[TestCase("test result")]
|
||||
public void should_replace_spaces_with_dash(string input)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input).Should().Be("test-result");
|
||||
}
|
||||
|
||||
[TestCase("test result", "test-result")]
|
||||
[TestCase("test-----result", "test-result")]
|
||||
[TestCase("test_____result", "test_result")]
|
||||
public void should_replace_double_occurence(string input, string result)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input).Should().Be(result);
|
||||
}
|
||||
|
||||
[TestCase("Test'Result")]
|
||||
[TestCase("Test$Result")]
|
||||
[TestCase("Test(Result")]
|
||||
[TestCase("Test)Result")]
|
||||
[TestCase("Test*Result")]
|
||||
[TestCase("Test?Result")]
|
||||
[TestCase("Test/Result")]
|
||||
[TestCase("Test=Result")]
|
||||
[TestCase("Test\\Result")]
|
||||
public void should_replace_special_characters_with_dash_when_enabled(string input)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input, true).Should().Be("test-result");
|
||||
}
|
||||
|
||||
[TestCase("Test'Result")]
|
||||
[TestCase("Test$Result")]
|
||||
[TestCase("Test(Result")]
|
||||
[TestCase("Test)Result")]
|
||||
[TestCase("Test*Result")]
|
||||
[TestCase("Test?Result")]
|
||||
[TestCase("Test/Result")]
|
||||
[TestCase("Test=Result")]
|
||||
[TestCase("Test\\Result")]
|
||||
public void should__not_replace_special_characters_with_dash_when_disabled(string input)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input, false).Should().Be("testresult");
|
||||
}
|
||||
|
||||
[TestCase("test----", "-_", "test")]
|
||||
[TestCase("test____", "-_", "test")]
|
||||
[TestCase("test-_-_", "-_", "test")]
|
||||
[TestCase("test----", "-", "test")]
|
||||
[TestCase("test____", "-", "test____")]
|
||||
[TestCase("test-_-_", "-", "test-_-_")]
|
||||
[TestCase("test----", "_", "test----")]
|
||||
[TestCase("test____", "_", "test")]
|
||||
[TestCase("test-_-_", "_", "test-_-")]
|
||||
[TestCase("test----", "", "test----")]
|
||||
[TestCase("test____", "", "test____")]
|
||||
[TestCase("test-_-_", "", "test-_-_")]
|
||||
public void should_trim_trailing_dashes_and_underscores_based_on_list(string input, string trimList, string result)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input, false, trimList, "").Should().Be(result);
|
||||
}
|
||||
|
||||
[TestCase("test----result", "-_", "test-result")]
|
||||
[TestCase("test____result", "-_", "test_result")]
|
||||
[TestCase("test_-_-result", "-_", "test-result")]
|
||||
[TestCase("test-_-_result", "-_", "test_result")]
|
||||
[TestCase("test----result", "-", "test-result")]
|
||||
[TestCase("test____result", "-", "test____result")]
|
||||
[TestCase("test-_-_result", "-", "test-_-_result")]
|
||||
[TestCase("test----result", "_", "test----result")]
|
||||
[TestCase("test____result", "_", "test_result")]
|
||||
[TestCase("test-_-_result", "_", "test-_-_result")]
|
||||
[TestCase("test----result", "", "test----result")]
|
||||
[TestCase("test____result", "", "test____result")]
|
||||
[TestCase("test-_-_result", "", "test-_-_result")]
|
||||
public void should_replace_duplicate_characters_based_on_list(string input, string deduplicateChars, string result)
|
||||
{
|
||||
Parser.Parser.ToUrlSlug(input, false, "", deduplicateChars).Should().Be(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_handle_null_trim_parameters()
|
||||
{
|
||||
Parser.Parser.ToUrlSlug("test", false, null, "-_").Should().Be("test");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_handle_null_dedupe_parameters()
|
||||
{
|
||||
Parser.Parser.ToUrlSlug("test", false, "-_", null).Should().Be("test");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_handle_empty_trim_parameters()
|
||||
{
|
||||
Parser.Parser.ToUrlSlug("test", false, "", "-_").Should().Be("test");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_handle_empty_dedupe_parameters()
|
||||
{
|
||||
Parser.Parser.ToUrlSlug("test", false, "-_", "").Should().Be("test");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -151,5 +152,107 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
||||
unmappedFolders.Count.Should().BeGreaterThan(0);
|
||||
unmappedFolders.Should().NotContain(u => u.Name == subFolder);
|
||||
}
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase(null)]
|
||||
public void should_handle_non_configured_recycle_bin(string recycleBinPath)
|
||||
{
|
||||
var rootFolder = Builder<RootFolder>.CreateNew()
|
||||
.With(r => r.Path = @"C:\Test\TV")
|
||||
.Build();
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
rootFolder = Builder<RootFolder>.CreateNew()
|
||||
.With(r => r.Path = @"/Test/TV")
|
||||
.Build();
|
||||
}
|
||||
|
||||
var subFolders = new[]
|
||||
{
|
||||
"Series1",
|
||||
"Series2",
|
||||
"Series3"
|
||||
};
|
||||
|
||||
var folders = subFolders.Select(f => Path.Combine(@"C:\Test\TV", f)).ToArray();
|
||||
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
folders = subFolders.Select(f => Path.Combine(@"/Test/TV", f)).ToArray();
|
||||
}
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.RecycleBin)
|
||||
.Returns(recycleBinPath);
|
||||
|
||||
Mocker.GetMock<IRootFolderRepository>()
|
||||
.Setup(s => s.Get(It.IsAny<int>()))
|
||||
.Returns(rootFolder);
|
||||
|
||||
Mocker.GetMock<IMovieRepository>()
|
||||
.Setup(s => s.AllMoviePaths())
|
||||
.Returns(new Dictionary<int, string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(rootFolder.Path))
|
||||
.Returns(folders);
|
||||
|
||||
var unmappedFolders = Subject.Get(rootFolder.Id, true).UnmappedFolders;
|
||||
|
||||
unmappedFolders.Count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_exclude_recycle_bin()
|
||||
{
|
||||
var rootFolder = Builder<RootFolder>.CreateNew()
|
||||
.With(r => r.Path = @"C:\Test\TV")
|
||||
.Build();
|
||||
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
rootFolder = Builder<RootFolder>.CreateNew()
|
||||
.With(r => r.Path = @"/Test/TV")
|
||||
.Build();
|
||||
}
|
||||
|
||||
var subFolders = new[]
|
||||
{
|
||||
"Series1",
|
||||
"Series2",
|
||||
"Series3",
|
||||
"BIN"
|
||||
};
|
||||
|
||||
var folders = subFolders.Select(f => Path.Combine(@"C:\Test\TV", f)).ToArray();
|
||||
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
folders = subFolders.Select(f => Path.Combine(@"/Test/TV", f)).ToArray();
|
||||
}
|
||||
|
||||
var recycleFolder = Path.Combine(OsInfo.IsNotWindows ? @"/Test/TV" : @"C:\Test\TV", "BIN");
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.RecycleBin)
|
||||
.Returns(recycleFolder);
|
||||
|
||||
Mocker.GetMock<IRootFolderRepository>()
|
||||
.Setup(s => s.Get(It.IsAny<int>()))
|
||||
.Returns(rootFolder);
|
||||
|
||||
Mocker.GetMock<IMovieRepository>()
|
||||
.Setup(s => s.AllMoviePaths())
|
||||
.Returns(new Dictionary<int, string>());
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(rootFolder.Path))
|
||||
.Returns(folders);
|
||||
|
||||
var unmappedFolders = Subject.Get(rootFolder.Id, true).UnmappedFolders;
|
||||
|
||||
unmappedFolders.Count.Should().Be(3);
|
||||
unmappedFolders.Should().NotContain(u => u.Name == "BIN");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +428,8 @@ namespace NzbDrone.Core.Configuration
|
||||
public CertificateValidationType CertificateValidation =>
|
||||
GetValueEnum("CertificateValidation", CertificateValidationType.Enabled);
|
||||
|
||||
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
||||
|
||||
private string GetValue(string key)
|
||||
{
|
||||
return GetValue(key, string.Empty);
|
||||
|
||||
@@ -104,5 +104,6 @@ namespace NzbDrone.Core.Configuration
|
||||
int BackupRetention { get; }
|
||||
|
||||
CertificateValidationType CertificateValidation { get; }
|
||||
string ApplicationUrl { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
@@ -18,6 +19,8 @@ namespace NzbDrone.Core.CustomFormats
|
||||
return (ICustomFormatSpecification)MemberwiseClone();
|
||||
}
|
||||
|
||||
public abstract NzbDroneValidationResult Validate();
|
||||
|
||||
public bool IsSatisfiedBy(ParsedMovieInfo movieInfo)
|
||||
{
|
||||
var match = IsSatisfiedByWithoutNegate(movieInfo);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
@@ -11,6 +12,8 @@ namespace NzbDrone.Core.CustomFormats
|
||||
bool Negate { get; set; }
|
||||
bool Required { get; set; }
|
||||
|
||||
NzbDroneValidationResult Validate();
|
||||
|
||||
ICustomFormatSpecification Clone();
|
||||
bool IsSatisfiedBy(ParsedMovieInfo movieInfo);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class IndexerFlagSpecificationValidator : AbstractValidator<IndexerFlagSpecification>
|
||||
{
|
||||
public IndexerFlagSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty();
|
||||
RuleFor(c => c.Value).Custom((qualityValue, context) =>
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(IndexerFlags), qualityValue))
|
||||
{
|
||||
context.AddFailure(string.Format("Invalid indexer flag condition value: {0}", qualityValue));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class IndexerFlagSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly IndexerFlagSpecificationValidator Validator = new IndexerFlagSpecificationValidator();
|
||||
|
||||
public override int Order => 4;
|
||||
public override string ImplementationName => "Indexer Flag";
|
||||
|
||||
@@ -17,5 +37,10 @@ namespace NzbDrone.Core.CustomFormats
|
||||
var flags = movieInfo?.ExtraInfo?.GetValueOrDefault("IndexerFlags") as IndexerFlags?;
|
||||
return flags?.HasFlag((IndexerFlags)Value) == true;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class LanguageSpecificationValidator : AbstractValidator<LanguageSpecification>
|
||||
{
|
||||
public LanguageSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty();
|
||||
RuleFor(c => c.Value).Custom((value, context) =>
|
||||
{
|
||||
if (!Language.All.Any(o => o.Id == value))
|
||||
{
|
||||
context.AddFailure(string.Format("Invalid Language condition value: {0}", value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class LanguageSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly LanguageSpecificationValidator Validator = new LanguageSpecificationValidator();
|
||||
|
||||
public override int Order => 3;
|
||||
public override string ImplementationName => "Language";
|
||||
|
||||
@@ -14,10 +34,15 @@ namespace NzbDrone.Core.CustomFormats
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(ParsedMovieInfo movieInfo)
|
||||
{
|
||||
var comparedLanguage = movieInfo != null && Name == "Original" && movieInfo.ExtraInfo.ContainsKey("OriginalLanguage")
|
||||
var comparedLanguage = movieInfo != null && Value == Language.Original.Id && movieInfo.ExtraInfo.ContainsKey("OriginalLanguage")
|
||||
? (Language)movieInfo.ExtraInfo["OriginalLanguage"]
|
||||
: (Language)Value;
|
||||
return movieInfo?.Languages?.Contains(comparedLanguage) ?? false;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
using System;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class QualityModifierSpecificationValidator : AbstractValidator<QualityModifierSpecification>
|
||||
{
|
||||
public QualityModifierSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty();
|
||||
RuleFor(c => c.Value).Custom((qualityValue, context) =>
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(Modifier), qualityValue))
|
||||
{
|
||||
context.AddFailure(string.Format("Invalid quality modifier condition value: {0}", qualityValue));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class QualityModifierSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly QualityModifierSpecificationValidator Validator = new QualityModifierSpecificationValidator();
|
||||
|
||||
public override int Order => 7;
|
||||
public override string ImplementationName => "Quality Modifier";
|
||||
|
||||
@@ -16,5 +36,10 @@ namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
return (movieInfo?.Quality?.Quality?.Modifier ?? (int)Modifier.NONE) == (Modifier)Value;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class RegexSpecificationBaseValidator : AbstractValidator<RegexSpecificationBase>
|
||||
{
|
||||
public RegexSpecificationBaseValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty().WithMessage("Regex Pattern must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class RegexSpecificationBase : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly RegexSpecificationBaseValidator Validator = new RegexSpecificationBaseValidator();
|
||||
|
||||
protected Regex _regex;
|
||||
protected string _raw;
|
||||
|
||||
[FieldDefinition(1, Label = "Regular Expression")]
|
||||
[FieldDefinition(1, Label = "Regular Expression", HelpText = "Custom Format RegEx is Case Insensitive")]
|
||||
public string Value
|
||||
{
|
||||
get => _raw;
|
||||
set
|
||||
{
|
||||
_raw = value;
|
||||
_regex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
if (value.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_regex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,5 +45,10 @@ namespace NzbDrone.Core.CustomFormats
|
||||
|
||||
return _regex.IsMatch(compared);
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class ResolutionSpecificationValidator : AbstractValidator<ResolutionSpecification>
|
||||
{
|
||||
public ResolutionSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class ResolutionSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly ResolutionSpecificationValidator Validator = new ResolutionSpecificationValidator();
|
||||
|
||||
public override int Order => 6;
|
||||
public override string ImplementationName => "Resolution";
|
||||
|
||||
@@ -16,5 +28,10 @@ namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
return (movieInfo?.Quality?.Quality?.Resolution ?? (int)Resolution.Unknown) == Value;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class SizeSpecificationValidator : AbstractValidator<SizeSpecification>
|
||||
{
|
||||
public SizeSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Min).GreaterThanOrEqualTo(0);
|
||||
RuleFor(c => c.Max).GreaterThan(c => c.Min);
|
||||
}
|
||||
}
|
||||
|
||||
public class SizeSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly SizeSpecificationValidator Validator = new SizeSpecificationValidator();
|
||||
|
||||
public override int Order => 8;
|
||||
public override string ImplementationName => "Size";
|
||||
|
||||
@@ -21,5 +34,10 @@ namespace NzbDrone.Core.CustomFormats
|
||||
|
||||
return size > Min.Gigabytes() && size <= Max.Gigabytes();
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class SourceSpecificationValidator : AbstractValidator<SourceSpecification>
|
||||
{
|
||||
public SourceSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class SourceSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly SourceSpecificationValidator Validator = new SourceSpecificationValidator();
|
||||
|
||||
public override int Order => 5;
|
||||
public override string ImplementationName => "Source";
|
||||
|
||||
@@ -16,5 +28,10 @@ namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
return (movieInfo?.Quality?.Quality?.Source ?? (int)Source.UNKNOWN) == (Source)Value;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.Net.Sockets;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -124,6 +125,37 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/radarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
|
||||
}
|
||||
catch (NpgsqlException e)
|
||||
{
|
||||
if (e.InnerException is SocketException)
|
||||
{
|
||||
var retryCount = 3;
|
||||
|
||||
while (true)
|
||||
{
|
||||
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
||||
|
||||
try
|
||||
{
|
||||
_migrationController.Migrate(connectionString, migrationContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (--retryCount > 0)
|
||||
{
|
||||
System.Threading.Thread.Sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new RadarrStartupException(ex, "Error creating main database");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RadarrStartupException(e, "Error creating main database");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RadarrStartupException(e, "Error creating main database");
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
SortTitle = Parser.Parser.NormalizeTitle(collectionName),
|
||||
Added = added,
|
||||
QualityProfileId = qualityProfileId,
|
||||
RootFolderPath = rootFolderPath,
|
||||
RootFolderPath = rootFolderPath.TrimEnd('/', '\\', ' '),
|
||||
SearchOnAdd = true,
|
||||
MinimumAvailability = minimumAvailability
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(212)]
|
||||
public class postgres_update_timestamp_columns_to_with_timezone : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Blocklist").AlterColumn("Date").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("Blocklist").AlterColumn("PublishedDate").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("Collections").AlterColumn("Added").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("Collections").AlterColumn("LastInfoSync").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("Commands").AlterColumn("QueuedAt").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("Commands").AlterColumn("StartedAt").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("Commands").AlterColumn("EndedAt").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("DownloadClientStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("DownloadClientStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("DownloadClientStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("DownloadHistory").AlterColumn("Date").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("ExtraFiles").AlterColumn("Added").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("ExtraFiles").AlterColumn("LastUpdated").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("History").AlterColumn("Date").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("ImportListStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("ImportListStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("ImportListStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("IndexerStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("IndexerStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("IndexerStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("IndexerStatus").AlterColumn("CookiesExpirationDate").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("MetadataFiles").AlterColumn("LastUpdated").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("MetadataFiles").AlterColumn("Added").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("MovieFiles").AlterColumn("DateAdded").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("MovieMetadata").AlterColumn("DigitalRelease").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("MovieMetadata").AlterColumn("InCinemas").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("MovieMetadata").AlterColumn("LastInfoSync").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("MovieMetadata").AlterColumn("PhysicalRelease").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("Movies").AlterColumn("Added").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("PendingReleases").AlterColumn("Added").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("ScheduledTasks").AlterColumn("LastExecution").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("ScheduledTasks").AlterColumn("LastStartTime").AsDateTimeOffset().Nullable();
|
||||
Alter.Table("SubtitleFiles").AlterColumn("Added").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("SubtitleFiles").AlterColumn("LastUpdated").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("VersionInfo").AlterColumn("AppliedOn").AsDateTimeOffset().Nullable();
|
||||
}
|
||||
|
||||
protected override void LogDbUpgrade()
|
||||
{
|
||||
Alter.Table("Logs").AlterColumn("Time").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("UpdateHistory").AlterColumn("Date").AsDateTimeOffset().NotNullable();
|
||||
Alter.Table("VersionInfo").AlterColumn("AppliedOn").AsDateTimeOffset().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(214)]
|
||||
public class add_language_tags_to_subtitle_files : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("SubtitleFiles").AddColumn("LanguageTags").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
using System;
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class RepackSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly UpgradableSpecification _upgradableSpecification;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RepackSpecification(UpgradableSpecification upgradableSpecification, Logger logger)
|
||||
public RepackSpecification(UpgradableSpecification upgradableSpecification, IConfigService configService, Logger logger)
|
||||
{
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -22,11 +26,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks;
|
||||
|
||||
if (!subject.ParsedMovieInfo.Quality.Revision.IsRepack)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer)
|
||||
{
|
||||
_logger.Debug("Repacks are not preferred, skipping check");
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
if (subject.Movie.MovieFileId != 0)
|
||||
{
|
||||
var file = subject.Movie.MovieFile;
|
||||
@@ -36,6 +48,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
var releaseGroup = subject.ParsedMovieInfo.ReleaseGroup;
|
||||
var fileReleaseGroup = file.ReleaseGroup;
|
||||
|
||||
if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade)
|
||||
{
|
||||
_logger.Debug("Auto downloading of repacks is disabled");
|
||||
return Decision.Reject("Repack downloading is disabled");
|
||||
}
|
||||
|
||||
if (fileReleaseGroup.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Decision.Reject("Unable to determine release group for the existing file");
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -31,7 +30,6 @@ namespace NzbDrone.Core.Extras
|
||||
public void Handle(MovieScannedEvent message)
|
||||
{
|
||||
var movie = message.Movie;
|
||||
var extraFiles = new List<ExtraFile>();
|
||||
|
||||
if (!_diskProvider.FolderExists(movie.Path))
|
||||
{
|
||||
@@ -43,17 +41,16 @@ namespace NzbDrone.Core.Extras
|
||||
var filesOnDisk = _diskScanService.GetNonVideoFiles(movie.Path);
|
||||
var possibleExtraFiles = _diskScanService.FilterPaths(movie.Path, filesOnDisk, false);
|
||||
|
||||
var filteredFiles = possibleExtraFiles;
|
||||
var importedFiles = new List<string>();
|
||||
|
||||
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
|
||||
{
|
||||
var imported = existingExtraFileImporter.ProcessFiles(movie, filteredFiles, importedFiles);
|
||||
var imported = existingExtraFileImporter.ProcessFiles(movie, possibleExtraFiles, importedFiles);
|
||||
|
||||
importedFiles.AddRange(imported.Select(f => Path.Combine(movie.Path, f.RelativePath)));
|
||||
}
|
||||
|
||||
_logger.Info("Found {0} extra files", extraFiles.Count);
|
||||
_logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ namespace NzbDrone.Core.Extras
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly List<IManageExtraFiles> _extraFileManagers;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ExtraService(IMediaFileService mediaFileService,
|
||||
IMovieService movieService,
|
||||
@@ -45,7 +44,6 @@ namespace NzbDrone.Core.Extras
|
||||
_diskProvider = diskProvider;
|
||||
_configService = configService;
|
||||
_extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void ImportMovie(LocalMovie localMovie, MovieFile movieFile, bool isReadOnly)
|
||||
@@ -62,61 +60,42 @@ namespace NzbDrone.Core.Extras
|
||||
return;
|
||||
}
|
||||
|
||||
var sourcePath = localMovie.Path;
|
||||
var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
|
||||
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
|
||||
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories).Where(f => f != localMovie.Path);
|
||||
var folderSearchOption = localMovie.FolderMovieInfo == null
|
||||
? SearchOption.TopDirectoryOnly
|
||||
: SearchOption.AllDirectories;
|
||||
|
||||
var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(e => e.Trim(' ', '.'))
|
||||
.Select(e => e.Trim(' ', '.')
|
||||
.Insert(0, "."))
|
||||
.ToList();
|
||||
|
||||
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||
var filteredFilenames = new List<string>();
|
||||
var hasNfo = false;
|
||||
var sourceFolder = _diskProvider.GetParentFolder(localMovie.Path);
|
||||
var files = _diskProvider.GetFiles(sourceFolder, folderSearchOption);
|
||||
var managedFiles = _extraFileManagers.Select((i) => new List<string>()).ToArray();
|
||||
|
||||
foreach (var matchingFilename in matchingFilenames)
|
||||
foreach (var file in files)
|
||||
{
|
||||
// Filter out duplicate NFO files
|
||||
if (matchingFilename.EndsWith(".nfo", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (hasNfo)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasNfo = true;
|
||||
}
|
||||
|
||||
filteredFilenames.Add(matchingFilename);
|
||||
}
|
||||
|
||||
foreach (var matchingFilename in filteredFilenames)
|
||||
{
|
||||
var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e));
|
||||
var extension = Path.GetExtension(file);
|
||||
var matchingExtension = wantedExtensions.FirstOrDefault(e => e.Equals(extension));
|
||||
|
||||
if (matchingExtension == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
for (int i = 0; i < _extraFileManagers.Count; i++)
|
||||
{
|
||||
foreach (var extraFileManager in _extraFileManagers)
|
||||
if (_extraFileManagers[i].CanImportFile(localMovie, movieFile, file, extension, isReadOnly))
|
||||
{
|
||||
var extension = Path.GetExtension(matchingFilename);
|
||||
var extraFile = extraFileManager.Import(localMovie.Movie, movieFile, matchingFilename, extension, isReadOnly);
|
||||
|
||||
if (extraFile != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
managedFiles[i].Add(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to import extra file: {0}", matchingFilename);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _extraFileManagers.Count; i++)
|
||||
{
|
||||
_extraFileManagers[i].ImportFiles(localMovie, movieFile, managedFiles[i], isReadOnly);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Files
|
||||
{
|
||||
@@ -19,7 +20,8 @@ namespace NzbDrone.Core.Extras.Files
|
||||
IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile);
|
||||
IEnumerable<ExtraFile> CreateAfterMovieFolder(Movie movie, string movieFolder);
|
||||
IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles);
|
||||
ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly);
|
||||
bool CanImportFile(LocalMovie localMovie, MovieFile movieFile, string path, string extension, bool readOnly);
|
||||
IEnumerable<ExtraFile> ImportFiles(LocalMovie localMovie, MovieFile movieFile, List<string> files, bool isReadOnly);
|
||||
}
|
||||
|
||||
public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles
|
||||
@@ -47,7 +49,8 @@ namespace NzbDrone.Core.Extras.Files
|
||||
public abstract IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile);
|
||||
public abstract IEnumerable<ExtraFile> CreateAfterMovieFolder(Movie movie, string movieFolder);
|
||||
public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles);
|
||||
public abstract ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly);
|
||||
public abstract bool CanImportFile(LocalMovie localMovie, MovieFile movieFile, string path, string extension, bool readOnly);
|
||||
public abstract IEnumerable<ExtraFile> ImportFiles(LocalMovie localMovie, MovieFile movieFile, List<string> files, bool isReadOnly);
|
||||
|
||||
protected TExtraFile ImportFile(Movie movie, MovieFile movieFile, string path, bool readOnly, string extension, string fileNameSuffix = null)
|
||||
{
|
||||
|
||||
@@ -148,11 +148,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
|
||||
var details = new XElement("movie");
|
||||
|
||||
details.Add(new XElement("title", movieTranslation?.Title ?? movie.Title));
|
||||
var metadataTitle = movieTranslation?.Title ?? movie.Title;
|
||||
|
||||
details.Add(new XElement("title", metadataTitle));
|
||||
|
||||
details.Add(new XElement("originaltitle", movie.MovieMetadata.Value.OriginalTitle));
|
||||
|
||||
details.Add(new XElement("sorttitle", movie.MovieMetadata.Value.SortTitle));
|
||||
details.Add(new XElement("sorttitle", Parser.Parser.NormalizeTitle(metadataTitle)));
|
||||
|
||||
if (movie.MovieMetadata.Value.Ratings.Tmdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings.Imdb?.Votes > 0)
|
||||
{
|
||||
@@ -261,7 +263,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
|
||||
details.Add(new XElement("country"));
|
||||
|
||||
if (movie.MovieMetadata.Value.CollectionTitle != null)
|
||||
if (Settings.AddCollectionName && movie.MovieMetadata.Value.CollectionTitle != null)
|
||||
{
|
||||
var setElement = new XElement("set");
|
||||
|
||||
@@ -305,6 +307,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
|
||||
details.Add(new XElement("trailer", "plugin://plugin.video.youtube/play/?video_id=" + movie.MovieMetadata.Value.YouTubeTrailerId));
|
||||
|
||||
details.Add(new XElement("watched", watched));
|
||||
|
||||
if (movieFile.MediaInfo != null)
|
||||
{
|
||||
var sceneName = movieFile.GetSceneOrFileName();
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
MovieMetadataLanguage = (int)Language.English;
|
||||
MovieImages = true;
|
||||
UseMovieNfo = false;
|
||||
AddCollectionName = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Movie Metadata", Type = FieldType.Checkbox)]
|
||||
@@ -41,6 +42,9 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
[FieldDefinition(4, Label = "Use Movie.nfo", Type = FieldType.Checkbox, HelpText = "Radarr will write metadata to movie.nfo instead of the default <movie-filename>.nfo")]
|
||||
public bool UseMovieNfo { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Collection Name", Type = FieldType.Checkbox, HelpText = "Radarr will write the collection name to the .nfo file", Advanced = true)]
|
||||
public bool AddCollectionName { get; set; }
|
||||
|
||||
public bool IsValid => true;
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -13,6 +13,7 @@ using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.Extras.Others;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
@@ -191,9 +192,14 @@ namespace NzbDrone.Core.Extras.Metadata
|
||||
return movedFiles;
|
||||
}
|
||||
|
||||
public override ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly)
|
||||
public override bool CanImportFile(LocalMovie localMovie, MovieFile movieFile, string path, string extension, bool readOnly)
|
||||
{
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> ImportFiles(LocalMovie localMovie, MovieFile movieFile, List<string> files, bool isReadOnly)
|
||||
{
|
||||
return Enumerable.Empty<ExtraFile>();
|
||||
}
|
||||
|
||||
private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> movieMetadata)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -7,13 +9,16 @@ using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Others
|
||||
{
|
||||
public class OtherExtraService : ExtraFileManager<OtherExtraFile>
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IOtherExtraFileService _otherExtraFileService;
|
||||
private readonly IMediaFileAttributeService _mediaFileAttributeService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public OtherExtraService(IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
@@ -23,8 +28,10 @@ namespace NzbDrone.Core.Extras.Others
|
||||
Logger logger)
|
||||
: base(configService, diskProvider, diskTransferService, logger)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_otherExtraFileService = otherExtraFileService;
|
||||
_mediaFileAttributeService = mediaFileAttributeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override int Order => 2;
|
||||
@@ -69,17 +76,79 @@ namespace NzbDrone.Core.Extras.Others
|
||||
return movedFiles;
|
||||
}
|
||||
|
||||
public override ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly)
|
||||
public override bool CanImportFile(LocalMovie localMovie, MovieFile movieFile, string path, string extension, bool readOnly)
|
||||
{
|
||||
var extraFile = ImportFile(movie, movieFile, path, readOnly, extension, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (extraFile != null)
|
||||
public override IEnumerable<ExtraFile> ImportFiles(LocalMovie localMovie, MovieFile movieFile, List<string> files, bool isReadOnly)
|
||||
{
|
||||
var importedFiles = new List<ExtraFile>();
|
||||
var filteredFiles = files.Where(f => CanImportFile(localMovie, movieFile, f, Path.GetExtension(f), isReadOnly)).ToList();
|
||||
var sourcePath = localMovie.Path;
|
||||
var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
|
||||
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
|
||||
var matchingFiles = new List<string>();
|
||||
var hasNfo = false;
|
||||
|
||||
foreach (var file in filteredFiles)
|
||||
{
|
||||
_mediaFileAttributeService.SetFilePermissions(path);
|
||||
_otherExtraFileService.Upsert(extraFile);
|
||||
try
|
||||
{
|
||||
// Filter out duplicate NFO files
|
||||
if (file.EndsWith(".nfo", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (hasNfo)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasNfo = true;
|
||||
}
|
||||
|
||||
// Filename match
|
||||
if (Path.GetFileNameWithoutExtension(file).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
matchingFiles.Add(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Movie match
|
||||
var fileMovieInfo = Parser.Parser.ParseMoviePath(file) ?? new ParsedMovieInfo();
|
||||
|
||||
if (fileMovieInfo.MovieTitle == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fileMovieInfo.MovieTitle == localMovie.FileMovieInfo.MovieTitle &&
|
||||
fileMovieInfo.Year.Equals(localMovie.FileMovieInfo.Year))
|
||||
{
|
||||
matchingFiles.Add(file);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to import extra file: {0}", file);
|
||||
}
|
||||
}
|
||||
|
||||
return extraFile;
|
||||
foreach (string file in matchingFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var extraFile = ImportFile(localMovie.Movie, movieFile, file, isReadOnly, Path.GetExtension(file), null);
|
||||
_mediaFileAttributeService.SetFilePermissions(file);
|
||||
_otherExtraFileService.Upsert(extraFile);
|
||||
importedFiles.Add(extraFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to import extra file: {0}", file);
|
||||
}
|
||||
}
|
||||
|
||||
return importedFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
@@ -5,6 +6,17 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public class SubtitleFile : ExtraFile
|
||||
{
|
||||
public SubtitleFile()
|
||||
{
|
||||
LanguageTags = new List<string>();
|
||||
}
|
||||
|
||||
public Language Language { get; set; }
|
||||
|
||||
public string AggregateString => Language + LanguageTagsAsString + Extension;
|
||||
|
||||
public List<string> LanguageTags { get; set; }
|
||||
|
||||
private string LanguageTagsAsString => string.Join(".", LanguageTags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -9,13 +10,17 @@ using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.MovieImport;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public class SubtitleService : ExtraFileManager<SubtitleFile>
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IDetectSample _detectSample;
|
||||
private readonly ISubtitleFileService _subtitleFileService;
|
||||
private readonly IMediaFileAttributeService _mediaFileAttributeService;
|
||||
private readonly Logger _logger;
|
||||
@@ -23,11 +28,14 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
public SubtitleService(IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IDiskTransferService diskTransferService,
|
||||
IDetectSample detectSample,
|
||||
ISubtitleFileService subtitleFileService,
|
||||
IMediaFileAttributeService mediaFileAttributeService,
|
||||
Logger logger)
|
||||
: base(configService, diskProvider, diskTransferService, logger)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_detectSample = detectSample;
|
||||
_subtitleFileService = subtitleFileService;
|
||||
_mediaFileAttributeService = mediaFileAttributeService;
|
||||
_logger = logger;
|
||||
@@ -64,21 +72,16 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
foreach (var movieFile in movieFiles)
|
||||
{
|
||||
var groupedExtraFilesForMovieFile = subtitleFiles.Where(m => m.MovieFileId == movieFile.Id)
|
||||
.GroupBy(s => s.Language + s.Extension).ToList();
|
||||
.GroupBy(s => s.AggregateString).ToList();
|
||||
|
||||
foreach (var group in groupedExtraFilesForMovieFile)
|
||||
{
|
||||
var groupCount = group.Count();
|
||||
var copy = 1;
|
||||
|
||||
if (groupCount > 1)
|
||||
{
|
||||
_logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(movie.Path, movieFile.RelativePath));
|
||||
}
|
||||
|
||||
foreach (var subtitleFile in group)
|
||||
{
|
||||
var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1);
|
||||
var suffix = GetSuffix(subtitleFile.Language, copy, subtitleFile.LanguageTags, groupCount > 1);
|
||||
movedFiles.AddIfNotNull(MoveFile(movie, movieFile, subtitleFile, suffix));
|
||||
|
||||
copy++;
|
||||
@@ -91,25 +94,141 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
return movedFiles;
|
||||
}
|
||||
|
||||
public override ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly)
|
||||
public override bool CanImportFile(LocalMovie localEpisode, MovieFile movieFile, string path, string extension, bool readOnly)
|
||||
{
|
||||
if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)))
|
||||
{
|
||||
var language = LanguageParser.ParseSubtitleLanguage(path);
|
||||
var suffix = GetSuffix(language, 1, false);
|
||||
var subtitleFile = ImportFile(movie, movieFile, path, readOnly, extension, suffix);
|
||||
subtitleFile.Language = language;
|
||||
|
||||
_mediaFileAttributeService.SetFilePermissions(path);
|
||||
_subtitleFileService.Upsert(subtitleFile);
|
||||
|
||||
return subtitleFile;
|
||||
}
|
||||
|
||||
return null;
|
||||
return SubtitleFileExtensions.Extensions.Contains(extension.ToLowerInvariant());
|
||||
}
|
||||
|
||||
private string GetSuffix(Language language, int copy, bool multipleCopies = false)
|
||||
public override IEnumerable<ExtraFile> ImportFiles(LocalMovie localMovie, MovieFile movieFile, List<string> files, bool isReadOnly)
|
||||
{
|
||||
var importedFiles = new List<SubtitleFile>();
|
||||
|
||||
var filteredFiles = files.Where(f => CanImportFile(localMovie, movieFile, f, Path.GetExtension(f), isReadOnly)).ToList();
|
||||
|
||||
var sourcePath = localMovie.Path;
|
||||
var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
|
||||
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
|
||||
|
||||
var matchingFiles = new List<string>();
|
||||
|
||||
foreach (var file in filteredFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Filename match
|
||||
if (Path.GetFileNameWithoutExtension(file).StartsWithIgnoreCase(sourceFileName))
|
||||
{
|
||||
matchingFiles.Add(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Movie match
|
||||
var fileMovieInfo = Parser.Parser.ParseMoviePath(file) ?? new ParsedMovieInfo();
|
||||
|
||||
if (fileMovieInfo.MovieTitle == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fileMovieInfo.MovieTitle == localMovie.FileMovieInfo.MovieTitle &&
|
||||
fileMovieInfo.Year.Equals(localMovie.FileMovieInfo.Year))
|
||||
{
|
||||
matchingFiles.Add(file);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to import subtitle file: {0}", file);
|
||||
}
|
||||
}
|
||||
|
||||
// Use any sub if only episode in folder
|
||||
if (matchingFiles.Count == 0 && filteredFiles.Count > 0)
|
||||
{
|
||||
var videoFiles = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories)
|
||||
.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file)))
|
||||
.ToList();
|
||||
|
||||
if (videoFiles.Count() > 2)
|
||||
{
|
||||
return importedFiles;
|
||||
}
|
||||
|
||||
// Filter out samples
|
||||
videoFiles = videoFiles.Where(file =>
|
||||
{
|
||||
var sample = _detectSample.IsSample(localMovie.Movie.MovieMetadata, file);
|
||||
|
||||
if (sample == DetectSampleResult.Sample)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).ToList();
|
||||
|
||||
if (videoFiles.Count == 1)
|
||||
{
|
||||
matchingFiles.AddRange(filteredFiles);
|
||||
|
||||
_logger.Warn("Imported any available subtitle file for movie: {0}", localMovie);
|
||||
}
|
||||
}
|
||||
|
||||
var subtitleFiles = new List<SubtitleFile>();
|
||||
|
||||
foreach (string file in matchingFiles)
|
||||
{
|
||||
var language = LanguageParser.ParseSubtitleLanguage(file);
|
||||
var extension = Path.GetExtension(file);
|
||||
var languageTags = LanguageParser.ParseLanguageTags(file);
|
||||
var subFile = new SubtitleFile
|
||||
{
|
||||
Language = language,
|
||||
Extension = extension
|
||||
};
|
||||
subFile.LanguageTags = languageTags.ToList();
|
||||
subFile.RelativePath = PathExtensions.GetRelativePath(sourceFolder, file);
|
||||
subtitleFiles.Add(subFile);
|
||||
}
|
||||
|
||||
var groupedSubtitleFiles = subtitleFiles.GroupBy(s => s.AggregateString).ToList();
|
||||
|
||||
foreach (var group in groupedSubtitleFiles)
|
||||
{
|
||||
var groupCount = group.Count();
|
||||
var copy = 1;
|
||||
|
||||
foreach (var file in group)
|
||||
{
|
||||
var path = Path.Combine(sourceFolder, file.RelativePath);
|
||||
var language = file.Language;
|
||||
var extension = file.Extension;
|
||||
var suffix = GetSuffix(language, copy, file.LanguageTags, groupCount > 1);
|
||||
try
|
||||
{
|
||||
var subtitleFile = ImportFile(localMovie.Movie, movieFile, path, isReadOnly, extension, suffix);
|
||||
subtitleFile.Language = language;
|
||||
subtitleFile.LanguageTags = file.LanguageTags;
|
||||
|
||||
_mediaFileAttributeService.SetFilePermissions(path);
|
||||
_subtitleFileService.Upsert(subtitleFile);
|
||||
|
||||
importedFiles.Add(subtitleFile);
|
||||
|
||||
copy++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to import subtitle file: {0}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return importedFiles;
|
||||
}
|
||||
|
||||
private string GetSuffix(Language language, int copy, List<string> languageTags, bool multipleCopies = false)
|
||||
{
|
||||
var suffixBuilder = new StringBuilder();
|
||||
|
||||
@@ -125,6 +244,12 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
suffixBuilder.Append(IsoLanguages.Get(language).TwoLetterCode);
|
||||
}
|
||||
|
||||
if (languageTags.Any())
|
||||
{
|
||||
suffixBuilder.Append(".");
|
||||
suffixBuilder.Append(string.Join(".", languageTags));
|
||||
}
|
||||
|
||||
return suffixBuilder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.ImportLists
|
||||
private readonly IImportListStatusService _importListStatusService;
|
||||
private readonly IImportListMovieService _listMovieService;
|
||||
private readonly ISearchForNewMovie _movieSearch;
|
||||
private readonly IProvideMovieInfo _movieInfoService;
|
||||
private readonly IMovieMetadataService _movieMetadataService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -30,6 +31,7 @@ namespace NzbDrone.Core.ImportLists
|
||||
IImportListStatusService importListStatusService,
|
||||
IImportListMovieService listMovieService,
|
||||
ISearchForNewMovie movieSearch,
|
||||
IProvideMovieInfo movieInfoService,
|
||||
IMovieMetadataService movieMetadataService,
|
||||
Logger logger)
|
||||
{
|
||||
@@ -37,6 +39,7 @@ namespace NzbDrone.Core.ImportLists
|
||||
_importListStatusService = importListStatusService;
|
||||
_listMovieService = listMovieService;
|
||||
_movieSearch = movieSearch;
|
||||
_movieInfoService = movieInfoService;
|
||||
_movieMetadataService = movieMetadataService;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -84,20 +87,10 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
if (!importListReports.AnyFailure)
|
||||
{
|
||||
// TODO some opportunity to bulk map here if we had the tmdbIds
|
||||
var listMovies = importListReports.Movies.Select(x =>
|
||||
{
|
||||
// Is it existing in result
|
||||
var movie = result.Movies.FirstOrDefault(r => r.TmdbId == x.TmdbId);
|
||||
|
||||
if (movie != null)
|
||||
{
|
||||
movie.ListId = importList.Definition.Id;
|
||||
}
|
||||
|
||||
return movie ?? MapMovieReport(x);
|
||||
}).Where(x => x.TmdbId > 0).ToList();
|
||||
var alreadyMapped = result.Movies.Where(x => importListReports.Movies.Any(r => r.TmdbId == x.TmdbId));
|
||||
var listMovies = MapMovieReports(importListReports.Movies.Where(x => !result.Movies.Any(r => r.TmdbId == x.TmdbId)).ToList()).Where(x => x.TmdbId > 0).ToList();
|
||||
|
||||
listMovies.AddRange(alreadyMapped);
|
||||
listMovies = listMovies.DistinctBy(x => x.TmdbId).ToList();
|
||||
listMovies.ForEach(m => m.ListId = importList.Definition.Id);
|
||||
|
||||
@@ -148,13 +141,10 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
if (!importListReports.AnyFailure)
|
||||
{
|
||||
// TODO some opportunity to bulk map here if we had the tmdbIds
|
||||
var listMovies = importListReports.Movies.Select(x =>
|
||||
{
|
||||
return MapMovieReport(x);
|
||||
}).Where(x => x.TmdbId > 0).ToList();
|
||||
var listMovies = MapMovieReports(importListReports.Movies).Where(x => x.TmdbId > 0).ToList();
|
||||
|
||||
listMovies = listMovies.DistinctBy(x => x.TmdbId).ToList();
|
||||
listMovies.ForEach(m => m.ListId = importList.Definition.Id);
|
||||
|
||||
result.Movies.AddRange(listMovies);
|
||||
_listMovieService.SyncMoviesForList(listMovies, importList.Definition.Id);
|
||||
@@ -173,21 +163,31 @@ namespace NzbDrone.Core.ImportLists
|
||||
return result;
|
||||
}
|
||||
|
||||
private ImportListMovie MapMovieReport(ImportListMovie report)
|
||||
private List<ImportListMovie> MapMovieReports(List<ImportListMovie> reports)
|
||||
{
|
||||
var mappedMovie = _movieSearch.MapMovieToTmdbMovie(new MovieMetadata { Title = report.Title, TmdbId = report.TmdbId, ImdbId = report.ImdbId, Year = report.Year });
|
||||
var mappedMovies = reports.Select(m => _movieSearch.MapMovieToTmdbMovie(new MovieMetadata { Title = m.Title, TmdbId = m.TmdbId, ImdbId = m.ImdbId, Year = m.Year }))
|
||||
.Where(x => x != null)
|
||||
.DistinctBy(x => x.TmdbId)
|
||||
.ToList();
|
||||
|
||||
var mappedListMovie = new ImportListMovie { ListId = report.ListId };
|
||||
_movieMetadataService.UpsertMany(mappedMovies);
|
||||
|
||||
if (mappedMovie != null)
|
||||
var mappedListMovies = new List<ImportListMovie>();
|
||||
|
||||
foreach (var movieMeta in mappedMovies)
|
||||
{
|
||||
_movieMetadataService.Upsert(mappedMovie);
|
||||
var mappedListMovie = new ImportListMovie();
|
||||
|
||||
mappedListMovie.MovieMetadata = mappedMovie;
|
||||
mappedListMovie.MovieMetadataId = mappedMovie.Id;
|
||||
if (movieMeta != null)
|
||||
{
|
||||
mappedListMovie.MovieMetadata = movieMeta;
|
||||
mappedListMovie.MovieMetadataId = movieMeta.Id;
|
||||
}
|
||||
|
||||
mappedListMovies.Add(mappedListMovie);
|
||||
}
|
||||
|
||||
return mappedListMovie;
|
||||
return mappedListMovies;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
@@ -7,6 +8,7 @@ namespace NzbDrone.Core.ImportLists.ImportListMovies
|
||||
public interface IImportListMovieRepository : IBasicRepository<ImportListMovie>
|
||||
{
|
||||
List<ImportListMovie> GetAllForLists(List<int> listIds);
|
||||
bool ExistsByMetadataId(int metadataId);
|
||||
}
|
||||
|
||||
public class ImportListMovieRepository : BasicRepository<ImportListMovie>, IImportListMovieRepository
|
||||
@@ -20,5 +22,12 @@ namespace NzbDrone.Core.ImportLists.ImportListMovies
|
||||
{
|
||||
return Query(x => listIds.Contains(x.ListId));
|
||||
}
|
||||
|
||||
public bool ExistsByMetadataId(int metadataId)
|
||||
{
|
||||
var movies = Query(x => x.MovieMetadataId == metadataId);
|
||||
|
||||
return movies.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.ImportLists.ImportListMovies
|
||||
List<ImportListMovie> SyncMoviesForList(List<ImportListMovie> listMovies, int listId);
|
||||
void RemoveListMovie(ImportListMovie listMovie);
|
||||
ImportListMovie GetById(int id);
|
||||
bool ExistsByMetadataId(int metadataId);
|
||||
}
|
||||
|
||||
public class ImportListMovieService : IImportListMovieService, IHandleAsync<ProviderDeletedEvent<IImportList>>
|
||||
@@ -79,5 +80,10 @@ namespace NzbDrone.Core.ImportLists.ImportListMovies
|
||||
var moviesOnList = _importListMovieRepository.GetAllForLists(new List<int> { message.ProviderId });
|
||||
_importListMovieRepository.DeleteMany(moviesOnList);
|
||||
}
|
||||
|
||||
public bool ExistsByMetadataId(int metadataId)
|
||||
{
|
||||
return _importListMovieRepository.ExistsByMetadataId(metadataId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ namespace NzbDrone.Core.ImportLists.Radarr
|
||||
TagIds = Array.Empty<int>();
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Full URL", HelpText = "URL, including port, of the Radarr V3 instance to import from")]
|
||||
[FieldDefinition(0, Label = "Full URL", HelpText = "URL, including port, of the Radarr instance to import from (Radarr 3.0 or higher)")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "Apikey of the Radarr V3 instance to import from")]
|
||||
[FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "Apikey of the Radarr instance to import from (Radarr 3.0 or higher)")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "Profiles", HelpText = "Profiles from the source instance to import from")]
|
||||
|
||||
@@ -27,7 +27,13 @@ namespace NzbDrone.Core.ImportLists.Trakt.List
|
||||
{
|
||||
var link = string.Empty;
|
||||
|
||||
var listName = Parser.Parser.ToUrlSlug(Settings.Listname.Trim());
|
||||
// Trakt slug rules:
|
||||
// - replace all special characters with a dash
|
||||
// - replaces multiple dashes with a single dash
|
||||
// - allows underscore as a valid character
|
||||
// - does not trim underscore from the end
|
||||
// - allows multiple underscores in a row
|
||||
var listName = Parser.Parser.ToUrlSlug(Settings.Listname.Trim(), true, "-", "-");
|
||||
link += $"users/{Settings.Username.Trim()}/lists/{listName}/items/movies?limit={Settings.Limit}";
|
||||
|
||||
var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.Get, Settings.AccessToken));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Trakt.Popular
|
||||
@@ -9,6 +11,24 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular
|
||||
: base()
|
||||
{
|
||||
RuleFor(c => c.TraktListType).NotNull();
|
||||
|
||||
// Loose validation @TODO
|
||||
RuleFor(c => c.Rating)
|
||||
.Matches(@"^\d+\-\d+$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.Rating.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Not a valid rating");
|
||||
|
||||
// Any valid certification
|
||||
RuleFor(c => c.Certification)
|
||||
.Matches(@"^\bNR\b|\bG\b|\bPG\b|\bPG\-13\b|\bR\b|\bNC\-17\b$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.Certification.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Not a valid cerification");
|
||||
|
||||
// Loose validation @TODO
|
||||
RuleFor(c => c.Years)
|
||||
.Matches(@"^\d+(\-\d+)?$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.Years.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Not a valid year or range of years");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +39,25 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular
|
||||
public TraktPopularSettings()
|
||||
{
|
||||
TraktListType = (int)TraktPopularListType.Popular;
|
||||
Rating = "0-100";
|
||||
Certification = "NR,G,PG,PG-13,R,NC-17";
|
||||
Genres = "";
|
||||
Years = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktPopularListType), HelpText = "Type of list you're seeking to import from")]
|
||||
public int TraktListType { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Rating", HelpText = "Filter movies by rating range (0-100)")]
|
||||
public string Rating { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Certification", HelpText = "Filter movies by a certification (NR,G,PG,PG-13,R,NC-17), (Comma Separated)")]
|
||||
public string Certification { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Genres", HelpText = "Filter movies by Trakt Genre Slug (Comma Separated) Only for Popular Lists")]
|
||||
public string Genres { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Years", HelpText = "Filter movies by year or year range")]
|
||||
public string Years { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
@@ -29,24 +28,6 @@ namespace NzbDrone.Core.ImportLists.Trakt
|
||||
.WithMessage("Must authenticate with Trakt")
|
||||
.When(c => c.AccessToken.IsNotNullOrWhiteSpace() && c.RefreshToken.IsNotNullOrWhiteSpace());
|
||||
|
||||
// Loose validation @TODO
|
||||
RuleFor(c => c.Rating)
|
||||
.Matches(@"^\d+\-\d+$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.Rating.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Not a valid rating");
|
||||
|
||||
// Any valid certification
|
||||
RuleFor(c => c.Certification)
|
||||
.Matches(@"^\bNR\b|\bG\b|\bPG\b|\bPG\-13\b|\bR\b|\bNC\-17\b$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.Certification.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Not a valid cerification");
|
||||
|
||||
// Loose validation @TODO
|
||||
RuleFor(c => c.Years)
|
||||
.Matches(@"^\d+(\-\d+)?$", RegexOptions.IgnoreCase)
|
||||
.When(c => c.Years.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Not a valid year or range of years");
|
||||
|
||||
// Limit not smaller than 1 and not larger than 100
|
||||
RuleFor(c => c.Limit)
|
||||
.GreaterThan(0)
|
||||
@@ -62,10 +43,6 @@ namespace NzbDrone.Core.ImportLists.Trakt
|
||||
public TraktSettingsBase()
|
||||
{
|
||||
SignIn = "startOAuth";
|
||||
Rating = "0-100";
|
||||
Certification = "NR,G,PG,PG-13,R,NC-17";
|
||||
Genres = "";
|
||||
Years = "";
|
||||
Limit = 100;
|
||||
}
|
||||
|
||||
@@ -84,18 +61,6 @@ namespace NzbDrone.Core.ImportLists.Trakt
|
||||
[FieldDefinition(0, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public string AuthUser { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Rating", HelpText = "Filter movies by rating range (0-100)")]
|
||||
public string Rating { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Certification", HelpText = "Filter movies by a certification (NR,G,PG,PG-13,R,NC-17), (Comma Separated)")]
|
||||
public string Certification { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Genres", HelpText = "Filter movies by Trakt Genre Slug (Comma Separated) Only for Popular Lists")]
|
||||
public string Genres { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Years", HelpText = "Filter movies by year or year range")]
|
||||
public string Years { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Limit", HelpText = "Limit the number of movies to get")]
|
||||
public int Limit { get; set; }
|
||||
|
||||
|
||||
@@ -42,14 +42,15 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
foreach (var movieId in message.MovieIds)
|
||||
{
|
||||
var movie = _movieService.GetMovie(movieId);
|
||||
var userInvokedSearch = message.Trigger == CommandTrigger.Manual;
|
||||
|
||||
if (!movie.Monitored && message.Trigger != CommandTrigger.Manual)
|
||||
if (!movie.Monitored && !userInvokedSearch)
|
||||
{
|
||||
_logger.Debug("Movie {0} is not monitored, skipping search", movie.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
var decisions = _releaseSearchService.MovieSearch(movieId, false, false);
|
||||
var decisions = _releaseSearchService.MovieSearch(movieId, userInvokedSearch, false);
|
||||
downloadedCount += _processDownloadDecisions.ProcessDecisions(decisions).Grabbed.Count;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,12 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
|
||||
if (jsonResponse.Resource.error_code.HasValue)
|
||||
{
|
||||
if (jsonResponse.Resource.error_code == 20 || jsonResponse.Resource.error_code == 8
|
||||
|| jsonResponse.Resource.error_code == 9 || jsonResponse.Resource.error_code == 10)
|
||||
if (jsonResponse.Resource.error_code == 5 || jsonResponse.Resource.error_code == 8
|
||||
|| jsonResponse.Resource.error_code == 9 || jsonResponse.Resource.error_code == 10
|
||||
|| jsonResponse.Resource.error_code == 13 || jsonResponse.Resource.error_code == 14
|
||||
|| jsonResponse.Resource.error_code == 20)
|
||||
{
|
||||
// No results or imdbid not found
|
||||
// No results, rate limit, tmdbid not found/invalid, or imdbid not found/invalid
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
private static readonly SeedCriteriaSettingsValidator Validator = new SeedCriteriaSettingsValidator();
|
||||
|
||||
[FieldDefinition(0, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is download client's default", Advanced = true)]
|
||||
[FieldDefinition(0, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is download client's default. Ratio should be at least 1.0 and follow the indexers rules")]
|
||||
public double? SeedRatio { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Number, Label = "Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)]
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
if (InfoHashElementName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return item.FindDecendants(InfoHashElementName).FirstOrDefault().Value;
|
||||
return item.FindDecendants(InfoHashElementName).FirstOrDefault()?.Value;
|
||||
}
|
||||
|
||||
var magnetUrl = GetMagnetUrl(item);
|
||||
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
if (MagnetElementName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var magnetURL = item.FindDecendants(MagnetElementName).FirstOrDefault().Value;
|
||||
var magnetURL = item.FindDecendants(MagnetElementName).FirstOrDefault()?.Value;
|
||||
if (magnetURL.IsNotNullOrWhiteSpace() && magnetURL.StartsWith("magnet:"))
|
||||
{
|
||||
return magnetURL;
|
||||
|
||||
@@ -105,6 +105,8 @@ namespace NzbDrone.Core.Languages
|
||||
public static Language Ukrainian => new Language(32, "Ukrainian");
|
||||
public static Language Persian => new Language(33, "Persian");
|
||||
public static Language Bengali => new Language(34, "Bengali");
|
||||
public static Language Slovak => new Language(35, "Slovak");
|
||||
public static Language Latvian => new Language(36, "Latvian");
|
||||
public static Language Any => new Language(-1, "Any");
|
||||
public static Language Original => new Language(-2, "Original");
|
||||
|
||||
@@ -149,6 +151,8 @@ namespace NzbDrone.Core.Languages
|
||||
Ukrainian,
|
||||
Persian,
|
||||
Bengali,
|
||||
Slovak,
|
||||
Latvian,
|
||||
Any,
|
||||
Original
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -256,7 +256,7 @@
|
||||
"AlternativeTitle": "Alternative Titel",
|
||||
"AllMoviesHiddenDueToFilter": "Alle Filme sind wegen dem Filter ausgeblendet.",
|
||||
"Age": "Alter",
|
||||
"AddNewMovie": "Neuer Film...",
|
||||
"AddNewMovie": "Neuen Film hinzufügen",
|
||||
"AddList": "Liste hinzufügen",
|
||||
"SystemTimeCheckMessage": "Die Systemzeit ist um einen Tag versetzt. Bis die Zeit korrigiert wurde, könnten die geplanten Aufgaben nicht korrekt ausgeführt werden",
|
||||
"UnsavedChanges": "Ungespeicherte Änderungen",
|
||||
@@ -339,7 +339,7 @@
|
||||
"ChangeHasNotBeenSavedYet": "Änderung wurde noch nicht gespeichert",
|
||||
"CheckForFinishedDownloadsInterval": "Intervall zum prüfen von fertigen Downloads",
|
||||
"CleanLibraryLevel": "Mediathek aufräumen",
|
||||
"ClickToChangeLanguage": "Sprache ändern...",
|
||||
"ClickToChangeLanguage": "Sprache ändern ...",
|
||||
"ClickToChangeQuality": "Hier klicken um die Qualität zu ändern",
|
||||
"ClientPriority": "Priorität",
|
||||
"CloneFormatTag": "Format Tag kopieren",
|
||||
@@ -639,8 +639,8 @@
|
||||
"RetryingDownloadInterp": "Herunterladen nochmal versuchen {0} um {1}",
|
||||
"RemovingTag": "Tag entfernen",
|
||||
"ReleaseWillBeProcessedInterp": "Release wird verarbeitet {0}",
|
||||
"Queued": "Eingereiht",
|
||||
"QualityProfileDeleteConfirm": "Qualitätsprofil '{0}' wirklich löschen?",
|
||||
"Queued": "In der Warteschlange",
|
||||
"QualityProfileDeleteConfirm": "Qualitätsprofil '{0}' wirklich löschen",
|
||||
"Pending": "Ausstehend",
|
||||
"Paused": "Pausiert",
|
||||
"NegateHelpText": "Wenn aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.",
|
||||
@@ -653,7 +653,7 @@
|
||||
"MarkAsFailedMessageText": "'{0}' wirklich als fehlgeschlagen markieren?",
|
||||
"Manual": "Manuell",
|
||||
"LogLevelTraceHelpTextWarning": "Trace logging sollte nur kurzzeitig aktiviert werden",
|
||||
"LastDuration": "Letzte Laufzeit",
|
||||
"LastDuration": "Letzte Dauer",
|
||||
"IncludeRecommendationsHelpText": "Radarrs Empfehlungen in der Entdeckungsansicht mit einbeziehen",
|
||||
"IncludeRadarrRecommendations": "Radarr Empfehlungen einbeziehen",
|
||||
"ImportFailedInterp": "Import fehlgeschlagen: {0}",
|
||||
@@ -889,7 +889,7 @@
|
||||
"RadarrUpdated": "Radarr wurde aktualisiert",
|
||||
"RadarrCalendarFeed": "Radarr Kalender Feed",
|
||||
"QueueIsEmpty": "Warteschlange ist leer",
|
||||
"QualityProfileInUse": "Ein Qualitätsprofil mit zugeordneten Filmen kann nicht gelöscht werden",
|
||||
"QualityProfileInUse": "Ein Qualitätsprofil mit zugeordneten Filmen, Listen oder Sammlungen kann nicht gelöscht werden",
|
||||
"QualityOrLangCutoffHasNotBeenMet": "Qualität oder Sprache haben die Schwelle noch nicht erreicht",
|
||||
"QualityLimitsHelpText": "Limitierungen werden automatisch an die Filmlänge angepasst.",
|
||||
"QualitiesHelpText": "Qualitätsprofile oben sind mehr bevorzugt. Profile in einer Gruppe sind gleichgestellt. Nur ausgewählte werden gesucht",
|
||||
@@ -1047,7 +1047,7 @@
|
||||
"RemotePathMappingCheckFilesLocalWrongOSPath": "Downloader {0} meldet Dateien in {1}, aber dies ist kein valider {2} Pfad. Überprüfe die Downloader Einstellungen.",
|
||||
"RemotePathMappingCheckFilesBadDockerPath": "Docker erkannt; Downloader {0} meldet Dateien in {1}, aber dies ist kein valider {2} Pfad. Überprüfe deine Remote-Pfadzuordnungen und die Downloader Einstellungen.",
|
||||
"RemotePathMappingCheckFilesWrongOSPath": "Downloader {0} meldet Dateien in {1}, aber dies ist kein valider {2} Pfad. Überprüfe deine Remote-Pfadzuordnungen und die Downloader Einstellungen.",
|
||||
"RemotePathMappingCheckGenericPermissions": "Downloader {0} speichert Downloads in {1}, aber Radarr kann dieses Verzeichnis nicht sehen. Möchlicherweise müssen die Verzeichnisrechte angepasst werden.",
|
||||
"RemotePathMappingCheckGenericPermissions": "Downloader {0} speichert Downloads in {1}, aber Radarr kann dieses Verzeichnis nicht sehen. Möglicherweise müssen die Verzeichnisrechte angepasst werden.",
|
||||
"RemotePathMappingCheckWrongOSPath": "Downloader {0} speichert Downloads in {1}, aber dies ist kein valider {2} Pfad. Überprüfe die Remote-Pfadzuordnungen und die Downloader Einstellungen.",
|
||||
"RemotePathMappingCheckLocalWrongOSPath": "Downloader {0} speichert Downloads in {1}, aber dies ist kein valider {2} Pfad. Überprüfe die Downloader Einstellungen.",
|
||||
"RemotePathMappingCheckLocalFolderMissing": "Downloader {0} speichert Downloads in {1}, aber dieses Verzeichnis scheint nicht zu existieren. Möglicherweise eine fehlende oder falsche Remote-Pfadzuordnung.",
|
||||
@@ -1112,10 +1112,10 @@
|
||||
"OriginalTitle": "Originaler Titel",
|
||||
"OriginalLanguage": "Originale Sprache",
|
||||
"Database": "Datenbank",
|
||||
"AllCollectionsHiddenDueToFilter": "Alle Filme sind wegen dem Filter ausgeblendet.",
|
||||
"Collections": "Sammlung",
|
||||
"AllCollectionsHiddenDueToFilter": "Alle Filme sind auf Grund des Filters ausgeblendet.",
|
||||
"Collections": "Sammlungen",
|
||||
"MonitorMovies": "Film beobachten",
|
||||
"NoCollections": "Keine Filme gefunden. Zum Starten solltest du einen Film hinzufügen oder vorhandene Importieren.",
|
||||
"NoCollections": "Keine Filme gefunden. Zum Starten solltest du einen Film hinzufügen oder vorhandene Importieren",
|
||||
"RssSyncHelpText": "Intervall in Minuten. Zum deaktivieren auf 0 setzen ( Dies wird das automatische Release erfassen deaktivieren )",
|
||||
"CollectionOptions": "Sammlung Optionen",
|
||||
"ChooseImportMode": "Wähle eine Importmethode",
|
||||
@@ -1127,7 +1127,7 @@
|
||||
"ScrollMovies": "Filme scrollen",
|
||||
"ShowCollectionDetails": "Status der Sammlung anzeigen",
|
||||
"RefreshCollections": "Sammlungen aktualisieren",
|
||||
"RefreshMonitoredIntervalHelpText": "Wie häufig die beobachteten Downloads von Download-Clients aktualisiert werden sollen (Min. 1 Minute).",
|
||||
"RefreshMonitoredIntervalHelpText": "Wie häufig die beobachteten Downloads von Download-Clients aktualisiert werden sollen (min. 1 Minute)",
|
||||
"ShowOverview": "Übersicht anzeigen",
|
||||
"ShowPosters": "Plakate anzeigen",
|
||||
"CollectionShowDetailsHelpText": "Status und Eigenschaften der Sammlung anzeigen",
|
||||
@@ -1141,5 +1141,9 @@
|
||||
"OnMovieAddedHelpText": "Ausführen wenn ein Film hinzugefügt wurde",
|
||||
"UnableToLoadCollections": "Sammlungen können nicht geladen werden",
|
||||
"InstanceName": "Instanzname",
|
||||
"InstanceNameHelpText": "Instanzname im Browser-Tab und für Syslog-Anwendungsname"
|
||||
"InstanceNameHelpText": "Instanzname im Browser-Tab und für Syslog-Anwendungsname",
|
||||
"RottenTomatoesRating": "Tomato Bewertung",
|
||||
"TotalMovies": "Filme insgesamt",
|
||||
"ApplicationURL": "Anwendungs-URL",
|
||||
"ApplicationUrlHelpText": "Die externe URL der Anwendung inklusive http(s)://, Port und URL-Basis"
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
"ApiKey": "API Key",
|
||||
"AppDataDirectory": "AppData directory",
|
||||
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
|
||||
"ApplicationURL": "Application URL",
|
||||
"ApplicationUrlHelpText": "This application's external URL including http(s)://, port and URL base",
|
||||
"Apply": "Apply",
|
||||
"ApplyTags": "Apply Tags",
|
||||
"ApplyTagsHelpTexts1": "How to apply tags to the selected movies",
|
||||
@@ -735,7 +737,7 @@
|
||||
"QualityOrLangCutoffHasNotBeenMet": "Quality or Language cutoff has not been met",
|
||||
"QualityProfile": "Quality Profile",
|
||||
"QualityProfileDeleteConfirm": "Are you sure you want to delete the quality profile {0}",
|
||||
"QualityProfileInUse": "Can't delete a quality profile that is attached to a movie",
|
||||
"QualityProfileInUse": "Can't delete a quality profile that is attached to a movie, list, or collection",
|
||||
"QualityProfiles": "Quality Profiles",
|
||||
"QualitySettings": "Quality Settings",
|
||||
"QualitySettingsSummary": "Quality sizes and naming",
|
||||
@@ -1027,6 +1029,7 @@
|
||||
"Torrents": "Torrents",
|
||||
"TorrentsDisabled": "Torrents Disabled",
|
||||
"TotalFileSize": "Total File Size",
|
||||
"TotalMovies": "Total Movies",
|
||||
"TotalSpace": "Total Space",
|
||||
"Trace": "Trace",
|
||||
"Trailer": "Trailer",
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"Search": "Buscar",
|
||||
"Scheduled": "Programado",
|
||||
"SaveChanges": "Guardar Cambios",
|
||||
"RSSSync": "Sincronización RSS",
|
||||
"RSSSync": "Sincronizar RSS",
|
||||
"RootFolders": "Carpetas de Origen",
|
||||
"RootFolderCheckSingleMessage": "La carpeta de origen no existe: {0}",
|
||||
"RootFolderCheckMultipleMessage": "Varias carpetas de origen no existen: {0}",
|
||||
@@ -232,7 +232,7 @@
|
||||
"OAuthPopupMessage": "Pop-ups bloqueados por su navegador",
|
||||
"Name": "Nombre",
|
||||
"MoveFiles": "Mover Archivos",
|
||||
"Monitored": "Monitoreadas",
|
||||
"Monitored": "Monitoreada",
|
||||
"Message": "Mensaje",
|
||||
"Location": "Localización",
|
||||
"Level": "Nivel",
|
||||
@@ -461,7 +461,7 @@
|
||||
"ShowMonitoredHelpText": "Mostrar el estado de monitoreo debajo del poster",
|
||||
"ShowCutoffUnmetIconHelpText": "Mostrar el icono para los ficheros cuando no se ha alcanzado el corte",
|
||||
"ShowAsAllDayEvents": "Mostrar como Eventos de Todo el Día",
|
||||
"ShouldMonitorHelpText": "Si se habilita, las películas añadidas desde esta lista serán también monitoreadas",
|
||||
"ShouldMonitorHelpText": "Si las películas o colecciones en esta lista deberían ser agregadas como monitoreadas",
|
||||
"SetPermissionsLinuxHelpTextWarning": "Si no estas seguro de lo que hacen estos ajustes, no los modifiques.",
|
||||
"SetPermissionsLinuxHelpText": "Debe chmod ser ejecutado una vez los archivos hayan sido importados/renombrados?",
|
||||
"SetPermissions": "Ajustar Permisos",
|
||||
@@ -614,8 +614,8 @@
|
||||
"CleanLibraryLevel": "Limpiar el Nivel de la Librería",
|
||||
"BindAddress": "Dirección de Ligado",
|
||||
"OpenBrowserOnStart": "Abrir navegador al arrancar",
|
||||
"OnUpgradeHelpText": "Al Actualizar",
|
||||
"OnRenameHelpText": "En Renombrado",
|
||||
"OnUpgradeHelpText": "Al Mejorar La Calidad",
|
||||
"OnRenameHelpText": "Al Renombrar",
|
||||
"OnHealthIssueHelpText": "En Problema de Salud",
|
||||
"OnGrabHelpText": "Al Capturar",
|
||||
"SettingsRuntimeFormat": "Formato de Tiempo de ejecución",
|
||||
@@ -633,7 +633,7 @@
|
||||
"ShownClickToHide": "Mostrado, clic para ocultar",
|
||||
"ShowGenres": "Mostrar Géneros",
|
||||
"ShowCertification": "Mostrar Certificación",
|
||||
"SearchOnAddHelpText": "Buscar películas en esta lista cuando se añadan a Radarr",
|
||||
"SearchOnAddHelpText": "Buscar películas en esta lista cuando se añada a la biblioteca",
|
||||
"RSSSyncIntervalHelpTextWarning": "Se aplicará a todos los indexers, por favor sigue las reglas de los mismos",
|
||||
"RSSIsNotSupportedWithThisIndexer": "RSS no son soportadas por este indexer",
|
||||
"RetryingDownloadInterp": "Re-intentando descarga {0} en {1}",
|
||||
@@ -874,11 +874,11 @@
|
||||
"MovieFilesTotaling": "Totalización de archivos de película",
|
||||
"OnGrab": "Al Capturar",
|
||||
"OnHealthIssue": "En Problema de Salud",
|
||||
"OnImport": "Al importar",
|
||||
"OnImport": "Al Importar",
|
||||
"OnLatestVersion": "La última versión de Radarr ya está instalada",
|
||||
"OnlyTorrent": "Solo Torrent",
|
||||
"OnlyUsenet": "Solo Usenet",
|
||||
"OnRename": "En Renombrado",
|
||||
"OnRename": "Al Renombrar",
|
||||
"PreferTorrent": "Prefiero Torrent",
|
||||
"PreferUsenet": "Prefiero Usenet",
|
||||
"Presets": "Preajustes",
|
||||
@@ -988,12 +988,12 @@
|
||||
"Score": "Puntuación",
|
||||
"Script": "Guión",
|
||||
"SearchCutoffUnmet": "Corte de búsqueda no alcanzado",
|
||||
"SearchMissing": "Búsqueda que falta",
|
||||
"SearchMissing": "Buscar Faltantes",
|
||||
"Seconds": "Segundos",
|
||||
"SelectDotDot": "Seleccione...",
|
||||
"SelectLanguage": "Seleccione el idioma",
|
||||
"SelectMovie": "Seleccionar película",
|
||||
"SelectQuality": "Seleccionar calidad",
|
||||
"SelectLanguage": "Seleccionar Idioma",
|
||||
"SelectMovie": "Seleccionar Película",
|
||||
"SelectQuality": "Seleccionar Calidad",
|
||||
"Small": "Pequeña",
|
||||
"Socks4": "Calcetines4",
|
||||
"Socks5": "Socks5 (soporte TOR)",
|
||||
@@ -1031,13 +1031,13 @@
|
||||
"showCinemaReleaseHelpText": "Mostrar la fecha de estreno en el cine debajo del cartel",
|
||||
"ShowReleaseDate": "Mostrar fecha de lanzamiento",
|
||||
"ShowReleaseDateHelpText": "Mostrar la fecha de lanzamiento debajo del cartel",
|
||||
"OnMovieDelete": "Al eliminar la película",
|
||||
"OnMovieDeleteHelpText": "Al eliminar la película",
|
||||
"OnMovieFileDelete": "Al eliminar archivo de película",
|
||||
"OnMovieFileDeleteHelpText": "Al eliminar archivo de película",
|
||||
"OnMovieFileDeleteForUpgrade": "En archivo de película Eliminar para actualizar",
|
||||
"OnMovieFileDeleteForUpgradeHelpText": "En archivo de película Eliminar para actualizar",
|
||||
"OnUpgrade": "Al Actualizar",
|
||||
"OnMovieDelete": "Al Eliminar Película",
|
||||
"OnMovieDeleteHelpText": "Al Eliminar Película",
|
||||
"OnMovieFileDelete": "Al Eliminar Archivo De Película",
|
||||
"OnMovieFileDeleteHelpText": "Al Eliminar Archivo De Película",
|
||||
"OnMovieFileDeleteForUpgrade": "Al Eliminar Archivo De Pelicula Para Mejorar La Calidad",
|
||||
"OnMovieFileDeleteForUpgradeHelpText": "Al Eliminar Archivo De Pelicula Para Mejorar La Calidad",
|
||||
"OnUpgrade": "Al Mejorar La Calidad",
|
||||
"Reddit": "Reddit",
|
||||
"More": "Más",
|
||||
"Download": "Descargar",
|
||||
@@ -1064,7 +1064,7 @@
|
||||
"List": "Lista",
|
||||
"ManualImportSetReleaseGroup": "Importación manual - Configurar el grupo de lanzamiento",
|
||||
"NotificationTriggersHelpText": "Seleccione qué eventos deben activar esta notificación",
|
||||
"OnApplicationUpdate": "Al actualizar la aplicación",
|
||||
"OnApplicationUpdate": "Al Actualizar La Aplicación",
|
||||
"RemotePathMappingCheckFileRemoved": "El archivo {0} fue eliminado a mitad del proceso.",
|
||||
"RemotePath": "Ruta remota",
|
||||
"RemotePathMappingCheckBadDockerPath": "Está utilizando docker; el cliente de descarga {0} coloca las descargas en {1} pero esta no es una ruta válida {2}. Revisa tus mapeos de rutas remotas y la configuración del cliente de descarga.",
|
||||
@@ -1072,7 +1072,7 @@
|
||||
"RemotePathMappingCheckFilesBadDockerPath": "Está utilizando docker; el cliente de descarga {0} informó de archivos en {1} pero esta no es una ruta válida {2}. Revise sus mapeos de rutas remotas y la configuración del cliente de descarga.",
|
||||
"RemotePathMappingCheckLocalWrongOSPath": "El cliente de descarga local {0} coloca las descargas en {1} pero ésta no es una ruta válida {2}. Revise la configuración de su cliente de descarga.",
|
||||
"RemotePathMappingCheckRemoteDownloadClient": "El cliente de descarga remota {0} informó de la existencia de archivos en {1} pero este directorio no parece existir. Probablemente falta mapear la ruta remota.",
|
||||
"SelectLanguages": "Seleccionar idiomas",
|
||||
"SelectLanguages": "Seleccionar Idiomas",
|
||||
"IndexerDownloadClientHelpText": "Especifica qué cliente de descargas se utiliza para las descargas de este indexador",
|
||||
"AnnouncedMsg": "Película anunciada",
|
||||
"Auto": "Auto",
|
||||
@@ -1097,7 +1097,7 @@
|
||||
"TmdbRating": "TMDb valoración",
|
||||
"TmdbVotes": "Votos en TMDb",
|
||||
"Filters": "Filtros",
|
||||
"OnApplicationUpdateHelpText": "Al actualizar la aplicación",
|
||||
"OnApplicationUpdateHelpText": "Al Actualizar La Aplicación",
|
||||
"OriginalTitle": "Título original",
|
||||
"OriginalLanguage": "Idioma original",
|
||||
"Rating": "Puntuación",
|
||||
@@ -1108,7 +1108,7 @@
|
||||
"RemotePathMappingCheckFilesLocalWrongOSPath": "El cliente de descarga local {0} informó de la existencia de archivos en {1}, pero no es una ruta válida {2}. Revise la configuración de su cliente de descarga.",
|
||||
"RemoveSelectedItem": "Eliminar el elemento seleccionado",
|
||||
"RemoveSelectedItems": "Eliminar los elementos seleccionados",
|
||||
"SelectReleaseGroup": "Seleccionar el grupo de lanzamiento",
|
||||
"SelectReleaseGroup": "Seleccionar Grupo De Lanzamiento",
|
||||
"SetReleaseGroup": "Configurar el grupo de lanzamiento",
|
||||
"TaskUserAgentTooltip": "User-Agent proporcionado por la aplicación llamó a la API",
|
||||
"RssSyncHelpText": "Intervalo en minutos. Ajustar a cero para inhabilitar (esto dentendrá toda captura de estrenos automática)",
|
||||
@@ -1119,5 +1119,28 @@
|
||||
"ChooseImportMode": "Elegir Modo de Importación",
|
||||
"CollectionOptions": "Opciones de la Colección",
|
||||
"CollectionShowDetailsHelpText": "Mostrar el estado y propiedades de la colección",
|
||||
"CollectionShowOverviewsHelpText": "Mostrar resumenes de la colección"
|
||||
"CollectionShowOverviewsHelpText": "Mostrar resumenes de la colección",
|
||||
"CollectionShowPostersHelpText": "Mostrar póster de artículos de colección",
|
||||
"RefreshMonitoredIntervalHelpText": "Cada cuánto actualizar las descargas monitoreadeas desde los clientes de descarga, mínimo 1 minuto",
|
||||
"SearchOnAddCollectionHelpText": "Buscar películas en esta colección cuando se añada a la biblioteca",
|
||||
"TotalMovies": "Películas Totales",
|
||||
"CollectionsSelectedInterp": "{0} Colecciones Seleccionadas",
|
||||
"EditCollection": "Editar Colección",
|
||||
"MonitorCollection": "Monitorear Colección",
|
||||
"MonitoredCollectionHelpText": "Monitorear para que las películas de esta colección se añadan automáticamente a la biblioteca",
|
||||
"MovieAndCollection": "Película y Colección",
|
||||
"MovieCollectionMultipleMissingRoots": "Múltiples carpetas raices faltantes para colecciones de películas: {0}",
|
||||
"OnMovieAdded": "Al Agregar Pelicula",
|
||||
"OnMovieAddedHelpText": "Al Agregar Película",
|
||||
"RefreshCollections": "Actualizar Colecciones",
|
||||
"ShowCollectionDetails": "Mostrar Estado De Colección",
|
||||
"InstanceName": "Nombre de Instancia",
|
||||
"InstanceNameHelpText": "Nombre de instancia en pestaña y para nombre de aplicación en Syslog",
|
||||
"RottenTomatoesRating": "Tomato Rating",
|
||||
"ScrollMovies": "Desplazar Películas",
|
||||
"ShowPosters": "Mostrar Posters",
|
||||
"UnableToLoadCollections": "No se han podido cargar las colecciones",
|
||||
"MovieCollectionMissingRoot": "Carpeta raíz faltante para colección de películas: {0}",
|
||||
"MovieOnly": "Solo Película",
|
||||
"Database": "Base de Datos"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"RadarrSupportsAnyDownloadClient": "Radarr tukee kaikkien Newznab-yhteensopivien lataustyökalujen ohella myös monia muita alla listattuja torrent- ja Usenet-lataustyökaluja.",
|
||||
"Timeleft": "Aikaa jäljellä",
|
||||
"UpdateCheckStartupNotWritableMessage": "Päivitystä ei voi asentaa, koska käyttäjällä '{1}' ei ole kirjoitusoikeutta käynnistyskansioon '{0}'.",
|
||||
"ApplyTagsHelpTexts4": "– 'Korvaa' kaikki aiemmat tunnisteet tai poista kaikki tunnisteet jättämällä tyhjäksi",
|
||||
"ApplyTagsHelpTexts4": "- \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi.",
|
||||
"UnableToLoadUISettings": "Käyttöliittymän asetuksien lataus epäonnistui.",
|
||||
"NotAvailable": "Ei saatavilla",
|
||||
"AddingTag": "Tunniste lisätään",
|
||||
@@ -92,9 +92,9 @@
|
||||
"DBMigration": "Tietokannan siirto",
|
||||
"ApiKey": "API-avain",
|
||||
"AcceptConfirmationModal": "Hyväksy vahvistus",
|
||||
"ApplyTagsHelpTexts1": "Miten tunnisteita sovelletaan valittuihin elokuviin",
|
||||
"ApplyTagsHelpTexts2": "– 'Lisää' syötetyt tunnisteet aiempiin tunnisteisiin",
|
||||
"ApplyTagsHelpTexts3": "– 'Poista' ainoastaan syötetyt tunnisteet",
|
||||
"ApplyTagsHelpTexts1": "Tunnistetoimenpiteiden selitykset:",
|
||||
"ApplyTagsHelpTexts2": "- \"Lisää\" syötetyt tunnisteet aiempiin tunnisteisiin.",
|
||||
"ApplyTagsHelpTexts3": "- \"Poista\" tyhjentää syötetyt tunnisteet.",
|
||||
"AvailabilityDelay": "Saatavuusviive",
|
||||
"AvailabilityDelayHelpText": "Aika ennen käytettävissä olevaa päivämäärää tai sen jälkeen elokuvan etsimiseen",
|
||||
"BackupIntervalHelpText": "Prowlarrin tietokannan ja asetusten automaattisen varmuuskopioinnin suoritusaikaväli.",
|
||||
@@ -160,16 +160,16 @@
|
||||
"AddToDownloadQueue": "Lisää latausjonoon",
|
||||
"AfterManualRefresh": "Manuaalisen päivityksen jälkeen",
|
||||
"AllFiles": "Kaikki tiedostot",
|
||||
"AllMoviesInPathHaveBeenImported": "Kaikki kansion '{0}' elokuvat on tuotu",
|
||||
"AllResultsHiddenFilter": "Aktiivinen suodatin on piilottanut kaikki tulokset",
|
||||
"AllMoviesInPathHaveBeenImported": "Kaikki kansion {0} elokuvat on tuotu",
|
||||
"AllResultsHiddenFilter": "Kaikki tulokset on piilotettu aktiivisen suodattimen johdosta.",
|
||||
"Always": "Aina",
|
||||
"AnalyseVideoFiles": "Analysoi videotiedostot",
|
||||
"AnalyticsEnabledHelpText": "Lähetä nimettömiä käyttö- ja virhetietoja sovelluksen palvelimille. Tämä sisältää tietoja selaimestasi, verkkokäyttöliittymän sivujen käytöstä, virheraportoinnista sekä käyttöjärjestelmästäsi ja versiosta. Käytämme näitä tietoja ominaisuuksien ja virhekorjauksien painotukseen.",
|
||||
"AptUpdater": "Käytä APT:ta päivityksen asennukseen",
|
||||
"AreYouSureYouWantToDeleteFormat": "Haluatko varmasti poistaa muotoilutagin '{0}'?",
|
||||
"AreYouSureYouWantToDeleteThisImportListExclusion": "Haluatko varmasti poistaa poikkeussäännön?",
|
||||
"AreYouSureYouWantToDeleteFormat": "Haluatko varmasti poistaa muotoilutunnisteen \"{0}\"?",
|
||||
"AreYouSureYouWantToDeleteThisImportListExclusion": "Haluatko varmasti poistaa tämän poikkeussäännön?",
|
||||
"AreYouSureYouWantToRemoveSelectedItemsFromQueue": "Haluatko varmasti poistaa jonosta {0} kohdetta?",
|
||||
"ApplyTags": "Toimenpide tunnisteille",
|
||||
"ApplyTags": "Tunnistetoimenpide",
|
||||
"AuthBasic": "Perus (selaimen ponnahdus)",
|
||||
"AuthenticationMethodHelpText": "Vaadi käyttäjätunnus ja salasana.",
|
||||
"AuthForm": "Lomake (kirjautumissivu)",
|
||||
@@ -220,7 +220,7 @@
|
||||
"MoveFiles": "Siirrä tiedostoja",
|
||||
"MovieFiles": "Elokuvatiedostot",
|
||||
"MovieIsRecommend": "Elokuvaa suositellaan viimeaikaisen lisäyksen perusteella",
|
||||
"NextExecution": "Seuraava toteutus",
|
||||
"NextExecution": "Seuraava suoritus",
|
||||
"NoAltTitle": "Ei vaihtoehtoisia nimikkeitä.",
|
||||
"NoEventsFound": "Tapahtumia ei löytynyt",
|
||||
"NoLinks": "Ei linkkejä",
|
||||
@@ -253,7 +253,7 @@
|
||||
"ProxyType": "Välityspalvelimen tyyppi",
|
||||
"PtpOldSettingsCheckMessage": "Seuraavat PassThePopcorn-tietolähteet sisältävät vanhentuneita asetuksia, jotka olisi syytä päivittää: {0}",
|
||||
"QualitiesHelpText": "Listalla ylempänä olevia laatuja painotetaan. Saman ryhmän laadut ovat tasaveroisia. Valitse vain haluamasi laadut.",
|
||||
"QualityProfileInUse": "Elokuvaan liitettyä laatuprofiilia ei voi poistaa.",
|
||||
"QualityProfileInUse": "Elokuvaan, listaan tai kokelmaan liitettyä laatuprofiilia ei voida poistaa.",
|
||||
"QueueIsEmpty": "Jono on tyhjä",
|
||||
"RadarrCalendarFeed": "Radarr-kalenterisyöte",
|
||||
"RadarrUpdated": "Radarr Päivitetty",
|
||||
@@ -320,8 +320,8 @@
|
||||
"ImportErrors": "Tuontivirheet",
|
||||
"Imported": "Tuodut",
|
||||
"InvalidFormat": "Väärä muoto",
|
||||
"LastDuration": "Viimeisin kesto",
|
||||
"LastExecution": "Viimeinen toteutus",
|
||||
"LastDuration": "Edellinen kesto",
|
||||
"LastExecution": "Edellinen suoritus",
|
||||
"ListSyncLevelHelpTextWarning": "Elokuvatiedostot poistetaan pysyvästi, mikä voi johtaa kirjaston pyyhkimiseen, jos luettelosi ovat tyhjät",
|
||||
"ListExclusions": "Listojen poikkeussäännöt",
|
||||
"Indexer": "Tietolähde",
|
||||
@@ -397,7 +397,7 @@
|
||||
"Path": "Sijainti",
|
||||
"Paused": "Keskeytetty",
|
||||
"Permissions": "Käyttöoikeudet",
|
||||
"AllMoviesHiddenDueToFilter": "Käytössä oleva suodatin on piilottanut kaikki elokuvat.",
|
||||
"AllMoviesHiddenDueToFilter": "Kaikki elokuvat on piilotettu aktiivisen suodattimen johdosta.",
|
||||
"AllowHardcodedSubs": "Salli poltetut tekstitykset",
|
||||
"PhysicalRelease": "Fyysinen julkaisu",
|
||||
"Port": "Portti",
|
||||
@@ -409,7 +409,7 @@
|
||||
"ProtocolHelpText": "Valitse käytettävä (t) protokolla (t) ja mikä on suositeltava, kun valitset muuten yhtä suurten julkaisujen välillä",
|
||||
"EditDelayProfile": "Muokkaa viiveprofiilia",
|
||||
"ProxyBypassFilterHelpText": "Käytä erottimena ',' ja '*.' jokerimerkkinä aliverkkotunnuksille (esim. www.esimerkki.fi,*.esimerkki.fi)",
|
||||
"AllowHardcodedSubsHelpText": "Havaitut poltetut tekstitykset ladataan automaattisesti",
|
||||
"AllowHardcodedSubsHelpText": "Havaitut poltetut tekstitykset ladataan automaattisesti.",
|
||||
"QualityProfile": "Laatuprofiili",
|
||||
"QualityProfiles": "Laatuprofiilit",
|
||||
"QuickImport": "Siirrä automaattisesti",
|
||||
@@ -443,7 +443,7 @@
|
||||
"TotalFileSize": "Tiedoston koko",
|
||||
"TotalSpace": "Kokonaistila",
|
||||
"UpgradeAllowedHelpText": "Jos käytöstä poistettuja laatuja ei päivitetä.",
|
||||
"AreYouSureYouWantToDeleteThisDelayProfile": "Haluatko varmasti poistaa viiveprofiilin?",
|
||||
"AreYouSureYouWantToDeleteThisDelayProfile": "Haluatko varmasti poistaa tämän viiveprofiilin?",
|
||||
"Username": "Käyttäjätunnus",
|
||||
"WaitingToImport": "Odottaa tuontia",
|
||||
"DigitalRelease": "Digitaalinen julkaisu",
|
||||
@@ -473,8 +473,8 @@
|
||||
"ConnectionLost": "Yhteys on katkennut",
|
||||
"Backup": "Varmuuskopio",
|
||||
"ConnectionLostAutomaticMessage": "Radarr pyrkii muodostamaan yhteyden automaattisesti tai voit painaa alta \"Lataa uudelleen\".",
|
||||
"AddNewMessage": "On helppoa lisätä uusi elokuva: ala vain kirjoittamaan haluamasi elokuvan nimeä.",
|
||||
"AddNewTmdbIdMessage": "Voit tehdä hakuja myös elokuvan TMDb-tunnuksella (esim. 'tmdb:71663').",
|
||||
"AddNewMessage": "Uuden elokuvan lisäys on helppoa. Aloita vain haluamasi elokuvan nimen kirjoitus.",
|
||||
"AddNewTmdbIdMessage": "Voit etsiä myös elokuvan TMDb-tunnisteella (esim. 'tmdb:71663').",
|
||||
"BackupFolderHelpText": "Suhteelliset polut kohdistuvat sovelluksen AppData-kansioon.",
|
||||
"Connections": "Kytkennät",
|
||||
"ConnectSettings": "Kytkentöjen asetukset",
|
||||
@@ -787,7 +787,7 @@
|
||||
"RenameMovies": "Uudelleennimeä elokuvat",
|
||||
"RenameMoviesHelpText": "Jos uudelleennimeäminen ei ole käytössä, käytetään olemassa olevaa tiedostonimeä.",
|
||||
"Reorder": "Järjestä uudelleen",
|
||||
"Replace": "Korvata",
|
||||
"Replace": "Korvaa",
|
||||
"ReplaceIllegalCharacters": "Korvaa kielletyt merkit",
|
||||
"ReplaceIllegalCharactersHelpText": "Korvaa laittomat merkit. Jos ei käytössä, laittomat merkit poistetaan.",
|
||||
"ReplaceWithSpaceDashSpace": "Korvus: välilyönti-väliviiva-välilyönti",
|
||||
@@ -1116,7 +1116,7 @@
|
||||
"RssSyncHelpText": "Aikaväli minuutteina. Arvo nolla kytkee toiminnon pois käytöstä ja lopettaen samalla automaattisen julkaisujen kaappauksen täysin.",
|
||||
"InstanceName": "Instanssin nimi",
|
||||
"InstanceNameHelpText": "Instanssin nimi välilehdellä ja järjestelmälokissa",
|
||||
"AllCollectionsHiddenDueToFilter": "Käytössä oleva suodatin on piilottanut kaikki kokoelmat.",
|
||||
"AllCollectionsHiddenDueToFilter": "Kaikki kokoelmat on piilotettu aktiivisen suodattimen johdosta.",
|
||||
"Collections": "Kokoelmat",
|
||||
"MonitorMovies": "Valvo elokuvia",
|
||||
"NoCollections": "Kokoelmia ei löytynyt. Aloita lisäämällä uusi elokuva tai tuo joitakin olemassa olevia.",
|
||||
@@ -1142,5 +1142,8 @@
|
||||
"CollectionOptions": "Kokoelmien valinnat",
|
||||
"CollectionShowDetailsHelpText": "Näytä kokoelmien tila ja ominaisuudet",
|
||||
"CollectionShowPostersHelpText": "Näytä kokoelmien julisteet",
|
||||
"RottenTomatoesRating": "Tomaattiarvio"
|
||||
"RottenTomatoesRating": "Tomaattiarvio",
|
||||
"TotalMovies": "Elokuvia yhteensä",
|
||||
"ApplicationURL": "Sovelluksen URL-osoite",
|
||||
"ApplicationUrlHelpText": "Sovelluksen ulkoinen URL-osoite, johon sisältyy http(s)://, portti ja URL-perusta."
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"RemotePathMappings": "Mappages de chemins distants",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "La branche {0} n'est pas une branche de version Radarr valide, vous ne recevrez pas de mises à jour",
|
||||
"RefreshAndScan": "Actualiser et analyser",
|
||||
"Refresh": "Rafraîchir",
|
||||
"Refresh": "Actualiser",
|
||||
"Ratings": "Évaluations",
|
||||
"Queue": "File d'attente",
|
||||
"QualitySettingsSummary": "Tailles qualité et dénomination",
|
||||
@@ -318,7 +318,7 @@
|
||||
"ChangeHasNotBeenSavedYet": "Les changements n'ont pas encore été sauvegardés",
|
||||
"ChangeFileDate": "Changer la date du fichier",
|
||||
"CertificationCountryHelpText": "Choisir un pays pour les classifications de films",
|
||||
"CertificateValidationHelpText": "Change la rigueur de la vérification du certificat HTTPS. Ne changez rien sauf si vous comprenez les risques.",
|
||||
"CertificateValidationHelpText": "Modifier le degré de rigueur de la validation de la certification HTTPS. Ne pas changer à moins que vous ne compreniez les risques.",
|
||||
"CertificateValidation": "Validation du certificat",
|
||||
"BypassProxyForLocalAddresses": "Contourner le proxy pour les adresses locales",
|
||||
"Branch": "Branche",
|
||||
@@ -1114,10 +1114,33 @@
|
||||
"SetReleaseGroup": "Régler le groupe de publication",
|
||||
"RefreshMonitoredIntervalHelpText": "Intervalle en minutes entre chaque vérification des téléchargements, minimum 1 minute",
|
||||
"AllCollectionsHiddenDueToFilter": "Tous les films sont masqués en raison du filtre appliqué.",
|
||||
"Collections": "Collection",
|
||||
"Collections": "Collections",
|
||||
"MonitorMovies": "Surveiller le film",
|
||||
"NoCollections": "Aucun film trouvé, pour commencer, vous voudrez ajouter un nouveau film ou importer des films existants.",
|
||||
"RssSyncHelpText": "Intervalle en minutes. Mettre à zéro pour désactiver (cela arrêtera tous les téléchargements automatiques)",
|
||||
"CollectionsSelectedInterp": "{0} Collections(s) Sélectionnée(s)",
|
||||
"ChooseImportMode": "Mode d'importation"
|
||||
"ChooseImportMode": "Mode d'importation",
|
||||
"CollectionOptions": "Options de collection",
|
||||
"CollectionShowDetailsHelpText": "Afficher l'état et les propriétés de la collection",
|
||||
"CollectionShowOverviewsHelpText": "Afficher les aperçus des collections",
|
||||
"OnMovieAddedHelpText": "À l'ajout d'un film",
|
||||
"MovieAndCollection": "Film et collection",
|
||||
"ShowOverview": "Afficher l'aperçu",
|
||||
"MovieCollectionMissingRoot": "Dossier racine manquant pour la collection de films : {0}",
|
||||
"OnMovieAdded": "À l'ajout d'un film",
|
||||
"MonitorCollection": "Surveiller la collection",
|
||||
"MonitoredCollectionHelpText": "Surveiller pour ajouter automatiquement les films de cette collection à la bibliothèque",
|
||||
"MovieCollectionMultipleMissingRoots": "Plusieurs dossiers racine manquent pour les collections de films: {0}",
|
||||
"MovieOnly": "Film seulement",
|
||||
"RefreshCollections": "Actualiser les collections",
|
||||
"SearchOnAddCollectionHelpText": "Rechercher des films dans cette collection lorsqu'ils sont ajoutés à Radarr",
|
||||
"UnableToLoadCollections": "Impossible de charger les collections",
|
||||
"RottenTomatoesRating": "Classement Rotten Tomatoes",
|
||||
"ShowCollectionDetails": "Afficher l'état de la collection",
|
||||
"EditCollection": "Modifier la collection",
|
||||
"ApplicationURL": "URL de l'application",
|
||||
"ApplicationUrlHelpText": "URL externe de cette application, y compris http(s)://, le port et l'URL de base",
|
||||
"InstanceName": "Nom de l'instance",
|
||||
"InstanceNameHelpText": "Nom de l'instance dans l'onglet du navigateur et pour le nom d'application dans Syslog",
|
||||
"CollectionShowPostersHelpText": "Afficher les affiches des éléments de la collection"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"AddExclusion": "Kivétel hozzáadása",
|
||||
"Activity": "Aktivitás",
|
||||
"Error": "Hiba",
|
||||
"Ended": "Befejezve",
|
||||
"Ended": "Vége lett",
|
||||
"EnableCompletedDownloadHandlingHelpText": "A befejezett letöltések automatikus importálása a letöltési kliensből",
|
||||
"EnableColorImpairedModeHelpText": "Megváltoztatott színek, hogy a színvak felhasználók jobban meg tudják különböztetni a színkódolt információkat",
|
||||
"EnableAutomaticSearchHelpTextWarning": "Interaktív keresés esetén is felhasználható",
|
||||
@@ -434,7 +434,7 @@
|
||||
"RadarrSupportsAnyIndexer": "A Radarr minden indexert támogat, amely a Newznab szabványt használja, valamint az alább felsorolt egyéb indexereket.",
|
||||
"RadarrSupportsAnyDownloadClient": "A Radarr számos népszerű torrent és usenet letöltési ügyfelet támogat.",
|
||||
"QuickImport": "Automatikus Áthelyezés",
|
||||
"Queued": "Sorban",
|
||||
"Queued": "Sorba helyezve",
|
||||
"Queue": "Várakozási sor",
|
||||
"QualitySettingsSummary": "Minőségi méretek és elnevezés",
|
||||
"QualitySettings": "Minőségi beállítások",
|
||||
@@ -616,7 +616,7 @@
|
||||
"Level": "Szint",
|
||||
"LaunchBrowserHelpText": " Nyisson meg egy böngészőt, és az alkalmazás indításakor lépjen a Radarr kezdőlapjára.",
|
||||
"LastWriteTime": "Utolsó írási idő",
|
||||
"LastDuration": "Utolsó Hossza",
|
||||
"LastDuration": "Utolsó időtartam",
|
||||
"Languages": "Nyelvek",
|
||||
"LanguageHelpText": "Nyelv a Releasekhez",
|
||||
"Language": "Nyelv",
|
||||
@@ -896,7 +896,7 @@
|
||||
"HomePage": "Kezdőlap",
|
||||
"LogOnly": "Csak naplózás",
|
||||
"LastUsed": "Utoljára használt",
|
||||
"LastExecution": "Utoljára végrehajtott",
|
||||
"LastExecution": "Utolsó végrehajtás",
|
||||
"Large": "Óriási",
|
||||
"KeepAndUnmonitorMovie": "Megtartás és megfigyelés kikapcsolása a filmnél",
|
||||
"InvalidFormat": "Érvénytelen Formátum",
|
||||
@@ -963,7 +963,7 @@
|
||||
"RadarrUpdated": "A Radarr frissítve",
|
||||
"RadarrCalendarFeed": "Radarr Naptár Feed",
|
||||
"QueueIsEmpty": "A várakozási sor üres",
|
||||
"QualityProfileInUse": "A minőség profil ami már filmhez van rendelve, nem törölhető",
|
||||
"QualityProfileInUse": "A filmhez, listához vagy gyűjteményhez csatolt minőségi profil nem törölhető",
|
||||
"QualityLimitsHelpText": "A korlátozások automatikusan beállítódnak a film hossza szerint.",
|
||||
"QualitiesHelpText": "A nagyobb minőség a listában jobban preferált. Ugyenazon minőségek a csoportban egyenlőek. Csak a bejelölt minőségek a kívánt minőségek",
|
||||
"Qualities": "Minőségek",
|
||||
@@ -992,7 +992,7 @@
|
||||
"NoLinks": "Nincsenek Linkek",
|
||||
"NoEventsFound": "Nem található esemény",
|
||||
"NoAltTitle": "Nincs alternatív cím.",
|
||||
"NextExecution": "Következő futtatás",
|
||||
"NextExecution": "Következő végrehajtás",
|
||||
"Negated": "Megtagadva",
|
||||
"Negate": "Megtagadás",
|
||||
"MultiLanguage": "Többnyelvű",
|
||||
@@ -1105,7 +1105,7 @@
|
||||
"Never": "Soha",
|
||||
"Rating": "Értékelés",
|
||||
"SetReleaseGroup": "Kiadási csoport beállítása",
|
||||
"Started": "Elindult",
|
||||
"Started": "Elkezdődött",
|
||||
"Waiting": "Várakozás",
|
||||
"OnApplicationUpdateHelpText": "Alkalmazásfrissítésről",
|
||||
"SizeLimit": "Méretkorlát",
|
||||
@@ -1142,5 +1142,8 @@
|
||||
"OnMovieAddedHelpText": "Film hozzáadásakor",
|
||||
"ShowPosters": "Poszterek megjelenítése",
|
||||
"InstanceNameHelpText": "Példánynév a böngésző lapon és a syslog alkalmazás neve",
|
||||
"RottenTomatoesRating": "Tomato Értékelés"
|
||||
"RottenTomatoesRating": "Tomato Értékelés",
|
||||
"TotalMovies": "Összes film",
|
||||
"ApplicationUrlHelpText": "Az alkalmazás külső URL-címe, beleértve a \"http(s)://\"-t, a \"Portot\" és az \"URL-alapot\" is",
|
||||
"ApplicationURL": "Alkalmazás URL-je"
|
||||
}
|
||||
|
||||
1
src/NzbDrone.Core/Localization/Core/lv.json
Normal file
1
src/NzbDrone.Core/Localization/Core/lv.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -254,5 +254,6 @@
|
||||
"Language": "språk",
|
||||
"List": "Liste",
|
||||
"New": "Ny",
|
||||
"RemotePathMappings": "Ekstern portmapping"
|
||||
"RemotePathMappings": "Ekstern portmapping",
|
||||
"Languages": "språk"
|
||||
}
|
||||
|
||||
@@ -1006,7 +1006,7 @@
|
||||
"TorrentDelayTime": "Torrent-vertraging: {0}",
|
||||
"TorrentsDisabled": "Torrents uitgeschakeld",
|
||||
"Trace": "Spoor",
|
||||
"Trailer": "Aanhangwagen",
|
||||
"Trailer": "Trailer",
|
||||
"Trakt": "Trakt",
|
||||
"Trigger": "In gang zetten",
|
||||
"UnableToImportCheckLogs": "Gedownload - Kan niet importeren: controleer de logboeken voor details",
|
||||
@@ -1105,5 +1105,7 @@
|
||||
"RssSyncHelpText": "Tussentijd in minuten. Schakel uit met 0 (dit stopt het automatisch ophalen van uitgaves)",
|
||||
"NoCollections": "Geen films gevonden, om te beginnen, voeg een nieuwe film toe of importeer bestaande films.",
|
||||
"Collections": "Collectie",
|
||||
"MonitorMovies": "Bewaak Film"
|
||||
"MonitorMovies": "Bewaak Film",
|
||||
"ApplicationURL": "Applicatie URL",
|
||||
"ApplicationUrlHelpText": "De externe URL van deze applicatie inclusief http(s)://,Port en URL base"
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
"Presets": "Presety",
|
||||
"ProxyType": "Typ proxy",
|
||||
"PtpOldSettingsCheckMessage": "Następujące indeksatory PassThePopcorn mają przestarzałe ustawienia i należy je zaktualizować: {0}",
|
||||
"QualityProfileInUse": "Nie można usunąć profilu jakości dołączonego do filmu",
|
||||
"QualityProfileInUse": "Nie można usunąć profilu jakości przypisanego do filmu, listy bądź kolekcji",
|
||||
"QueueIsEmpty": "Kolejka jest pusta",
|
||||
"RadarrCalendarFeed": "Kanał kalendarza radarowego",
|
||||
"RadarrUpdated": "Radarr zaktualizowany",
|
||||
@@ -1060,9 +1060,9 @@
|
||||
"SelectLanguages": "Wybierz język",
|
||||
"AllCollectionsHiddenDueToFilter": "Wszystkie kolekcje są ukryte ze względu na zastosowany filtr.",
|
||||
"Collections": "Kolekcje",
|
||||
"RssSyncHelpText": "Odstęp w minutach. Ustaw na zero, aby wyłączyć (zatrzyma to wszystkie automatyczne przechwytywanie zwolnień)",
|
||||
"RssSyncHelpText": "Odstęp w minutach. Ustaw na zero, aby wyłączyć (zatrzyma to wszystkie automatyczne przechwytywanie)",
|
||||
"NoCollections": "Nie znaleziono żadnych filmów. Aby rozpocząć, musisz dodać nowy film lub zaimportować istniejące",
|
||||
"MonitorMovies": "Monitoruj film",
|
||||
"MonitorMovies": "Monitoruj filmy",
|
||||
"ClickToChangeReleaseGroup": "Kliknij, by zmienić grupę wydającą",
|
||||
"RemotePathMappingCheckDownloadPermissions": "Radarr widzi film {0}, lecz nie ma do niego dostępu. Najprawdopodobniej to wynik błędu w uprawnieniach dostępu.",
|
||||
"NotificationTriggersHelpText": "Wybierz zdarzenia, które mają uruchamiać to powiadomienie",
|
||||
@@ -1078,7 +1078,7 @@
|
||||
"ManualImportSetReleaseGroup": "Import ręczny - podaj nazwę grupy",
|
||||
"Never": "Nigdy",
|
||||
"RemotePathMappingCheckFilesWrongOSPath": "Zdalny klient pobierania {0} zgłosił pliki w {1}, lecz nie jest to poprawna ścieżka {2}. Zmień ustawienia zdalnego mapowania ścieżek i klienta pobierania.",
|
||||
"RemotePathMappingCheckGenericPermissions": "Klient pobierania {0} umieszcza pobrane pliki w {1}, lecz Radarr nie widzi tego folderu. Być może należy zmienić uprawnienia dostępu do folderu.",
|
||||
"RemotePathMappingCheckGenericPermissions": "Klient pobierania {0} umieszcza pobrane pliki w {1}, ale Radarr nie widzi tego folderu. Prawdopodobnie należy zmienić uprawnienia dostępu do folderu.",
|
||||
"RemoveFailed": "Usuń nieudane",
|
||||
"RemoveDownloadsAlert": "Ustawienia usuwania zostały przeniesione do ustawień poszczególnych klientów pobierania powyżej.",
|
||||
"ShowCollectionDetails": "Pokaż stan kolekcji",
|
||||
@@ -1141,5 +1141,9 @@
|
||||
"CollectionShowPostersHelpText": "Pokaż plakaty elementów kolekcji",
|
||||
"CollectionOptions": "Opcje Kolekcji",
|
||||
"CollectionShowDetailsHelpText": "Pokaż status i właściwości kolekcji",
|
||||
"CollectionShowOverviewsHelpText": "Pokaż przegląd kolekcji"
|
||||
"CollectionShowOverviewsHelpText": "Pokaż przegląd kolekcji",
|
||||
"TotalMovies": "Filmów całkowicie",
|
||||
"RottenTomatoesRating": "Ocena Tomato",
|
||||
"ApplicationUrlHelpText": "Zewnętrzny URL tej aplikacji zawiera http(s)://, port i adres URL",
|
||||
"ApplicationURL": "Link do aplikacji"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user