1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-17 21:26:22 -04:00

Compare commits

...

61 Commits

Author SHA1 Message Date
Mark McDowall
c2989ced7f Fixed integration test's path to Sonarr executable
(cherry picked from commit 652027821d140c4f04fc7ca5883043bad25d89bb)
2022-08-19 06:31:39 +00:00
Servarr
d730161800 Automated API Docs update 2022-08-18 22:45:47 -05:00
Qstick
66c1af0555 New: (API) Get Collection by TmdbId 2022-08-18 22:37:15 -05:00
Chris
dca00db317 Added: Ntfy provider for notifications. (#7455)
* Added: Ntfy provider for notifications.

* Changed: Clean up of validation and fields for ntfy

* Fixed: Unused title setting, and spelling issue.
2022-08-18 20:12:06 -05:00
Chris
812e5ac5a3 Fixed: Postgres secret regex now less greedy
[common]
2022-08-18 21:36:22 +01:00
Chris
d01bae92bf Fixed: Regex in log cleanser taking 10+ minutes on messages longer than 100k. (#7481) 2022-08-18 13:00:58 +01:00
Qstick
1a6bf51741 New: Add support for Plex Edition tags in naming 2022-08-16 23:04:46 -05:00
Qstick
f3e7843150 New: Make Plex imdb tags conditional 2022-08-16 23:04:46 -05:00
Qstick
886b9b1c05 Fixed: Correctly map movie by original title on import
Fixes #7348
2022-08-15 23:16:22 -05:00
Qstick
d8891ee4ea Fixed: UI Error on Collection Filter
Fixes #7474
2022-08-14 16:33:40 -05:00
Qstick
192dd9c137 Fixed: Allow 0 Min on Size CustomFormat Condition
Fixes #7476
2022-08-14 16:19:28 -05:00
Gylesie
b549fddf95 New: Add Slovak Language 2022-08-13 16:25:32 -05:00
Qstick
c1f538ed97 Bump version to 4.2.2 2022-08-13 15:48:22 -05:00
Weblate
e72f8097fb Translated using Weblate (Spanish) [skip ci]
Currently translated at 97.8% (1120 of 1145 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 22.4% (257 of 1145 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.8% (10 of 1145 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 0.1% (1 of 1145 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 99.9% (1144 of 1145 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 98.3% (1125 of 1144 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: M0C <pigers@gmail.com>
Co-authored-by: Michael Maldonado <michael@maldonado.tech>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Sytha <tharaud.sylvain@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: beefnoodle <acer.wang@protonmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2022-08-13 13:49:13 -05:00
Qstick
3eec088306 Regenerate yarn.lock 2022-08-13 13:31:46 -05:00
Qstick
ad097dd1a2 Bump Sentry to 3.20.1 2022-08-13 12:55:49 -05:00
Qstick
b4b38a5318 Bump dotnet to 6.0.8 2022-08-13 12:40:52 -05:00
psylenced
b0717a0803 Changed: Removed Tigole from ExceptionRelease match as is checked in ExceptionReleaseExact. 2022-08-13 12:21:35 -05:00
psylenced
4d1d08d345 Fixed: Tigole release group not being parsed and matched correctly, requiring manual import. 2022-08-13 12:21:35 -05:00
psylenced
e689817508 Fixed: Configured recycle bin is excluded from import. 2022-08-13 12:03:03 -05:00
Qstick
3b191caf16 Really fix Original Language in Language CF Specification
Co-Authored-By: François-Xavier Payet <fx.payet@tfdn.org>
2022-08-13 12:02:10 -05:00
Qstick
cc6ca0b067 Better Sentry Filtering for AggregateException children 2022-08-12 23:33:27 -05:00
Qstick
57cb63fb18 Run Postgres tests on 20.04 2022-08-12 23:32:58 -05:00
Qstick
20f709d22a Fixed: Blank Collection on MovieDetails when no Collection for Movie 2022-08-12 22:55:42 -05:00
Qstick
5d8775ac96 Remove non-functional filters for Trakt Lists
Fixes #7464
2022-08-12 22:42:38 -05:00
Qstick
4890972e16 Fixed: Original CF shouldn't need to be named "Original"
Fixes #7473
2022-08-12 22:31:41 -05:00
Qstick
40dc808f61 Fixed NullRef in Skyhook Proxy during List Sync 2022-08-12 22:19:24 -05:00
bakerboy448
97077e09d2 Fixed: Remove Notifiarr Environment Option 2022-08-09 20:23:26 -05:00
Chris
9ba7027d00 Fixed: Trakt list request now uses correct rules for generating slug (#7449)
* Changed: Parser.ToUrlSlug now has optional parameters to define how it works in edge cases based on provider.

* Fixed: Trakt list request now uses correct rules for generating slug on site.

* Added: Unit tests for slug parser.

* Fixed: Null and blank parameters to ToSlugUrl parser. Added tests.
2022-08-07 22:18:06 -05:00
Qstick
9903e70925 Fixed: Allow blank ReleaseGroup and Edition from MovieFile edit
Fixes #7453
2022-08-07 22:05:27 -05:00
Qstick
3a6f3666f5 Fixed: Don't process files that don't have a supported media file extension
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-08-07 21:54:39 -05:00
Qstick
915c66be50 Fixed: Avoid failure if list contains same movie but without tmdbid
Fixes #7454
2022-08-07 21:47:46 -05:00
javaisbetterthanpython
70b22e483a Fixed: Log correct path when moving movies (#7439)
Fixes #7440
2022-07-25 18:17:52 -05:00
Deathspike
cad1191da5 Fixed: Watch state not preserved on metadata rewrite (#7436) 2022-07-23 12:17:26 -05:00
Qstick
43910af127 Fixed: NullRefException in TorrentRssParser
From 6c494e9a92
2022-07-21 20:11:45 -05:00
Qstick
f01c477b81 Bump Version to 4.2.1 2022-07-21 15:31:32 -04:00
bakerboy448
0054318658 Fixed: Parse Group ZØNEHD 2022-07-18 22:23:41 -05:00
bakerboy448
03a3f4522a New: Parse Group HONE 2022-07-18 22:23:41 -05:00
bakerboy448
3d3562dcda New: (Discord) Include Custom Formats & Score On Grab
Fixes #6733
2022-07-18 21:24:37 -05:00
Weblate
7a079c5e0c Translated using Weblate (Catalan) [skip ci]
Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 0.1% (1 of 1145 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 99.9% (1144 of 1145 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 100.0% (1145 of 1145 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 98.3% (1125 of 1144 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: M0C <pigers@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Sytha <tharaud.sylvain@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2022-07-18 20:17:12 -05:00
Terebi42
4d70798f2f Fixed: User Triggered Auto Searches now ignores monitored status (#7422) 2022-07-18 18:25:46 -05:00
Robin Dadswell
d55864f869 Fixed: Postgres timezone issues (#7183)
[common]

Co-authored-by: ta264 <ta264@users.noreply.github.com>
2022-07-18 14:57:15 +01:00
Qstick
3c41c84fb0 Speed up and reduce meta calls for Imdb Lists when mapping 2022-07-17 12:57:00 -05:00
Qstick
eae9a6d6e0 Fixed: ImportListMovies not saved if from a list without TMDBIds 2022-07-17 12:55:13 -05:00
Mark Mckessock
867f8f5835 Match 'HQCAM' as CAM source (#7412)
* Add HQCAM source regex

* Add cam testcases
2022-07-15 23:09:12 -05:00
Qstick
0c81387cfb Fix RefreshMovieServiceFixture folder service mock 2022-07-15 22:36:35 -05:00
Qstick
c5fb5200de Fixed: Collections not deleted on Movie Delete 2022-07-15 22:08:25 -05:00
Qstick
cc306fcd36 Fixed: Bulk Collection RootFolder change failure 2022-07-15 21:57:32 -05:00
Qstick
2bb7984961 New: Collection Folder, Genre, QualityProfile Filters 2022-07-15 21:57:32 -05:00
Qstick
21e605452a Fixed: Trim RootFolderPath on Migration 2022-07-15 21:57:31 -05:00
Qstick
476f5b5bfd Avoid multiple metadata DB calls on list mapping 2022-07-15 21:57:31 -05:00
Qstick
b6920cfe82 Fixed: Prevent excluded movies from being added by collections 2022-07-15 21:57:31 -05:00
Qstick
e89b98d0f6 Fixed: Avoid NullRef in MapMovieToTmdbMovie 2022-07-14 22:08:11 -05:00
bakerboy448
1db690ad39 Fixed: Notifiarr - Better HTTP Error Handling
also quiet sentry
2022-07-14 19:08:16 -05:00
Qstick
d5c524719b Fix Nullref on Collection delete 2022-07-12 19:20:46 -05:00
bakerboy448
ced6586860 New: (Notifiarr) Custom Formats in OnGrab 2022-07-12 08:44:59 -05:00
Servarr
8b3019821a Automated API Docs update 2022-07-10 13:03:18 -05:00
Qstick
16ed68d5de New: Custom Format Spec Validation
Fixes #7405
2022-07-10 12:25:42 -05:00
Qstick
098a893083 Fixed: Don't fail on single failure for Discover bulk add
Fixes #7409
2022-07-09 19:11:16 -05:00
Qstick
548e3400b5 Remove general yarn restore key to avoid cross OS conflict 2022-07-09 18:59:15 -05:00
Weblate
5c31e3f1a2 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 100.0% (1144 of 1144 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 97.6% (1117 of 1144 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1144 of 1144 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Moritz Ellerbrock <github@elmoritz.eu>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: reloxx <reloxx@interia.pl>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2022-07-09 00:16:35 -05:00
90 changed files with 3670 additions and 1272 deletions

View File

@@ -1,7 +1,6 @@
{ {
"paths": [ "paths": [
"frontend/src/**/*.js", "frontend/src/**/*.js"
"src/NzbDrone.Core/Localization/Core/*.json"
], ],
"ignored": [ "ignored": [
"**/node_modules/**/*" "**/node_modules/**/*"

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '4.2.0' majorVersion: '4.2.2'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.300' dotnetVersion: '6.0.400'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
@@ -173,7 +173,6 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --frontend - bash: ./build.sh --frontend
@@ -550,7 +549,7 @@ stages:
Radarr__Postgres__Password: 'radarr' Radarr__Postgres__Password: 'radarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10 timeoutInMinutes: 10
@@ -577,6 +576,7 @@ stages:
-e POSTGRES_PASSWORD=radarr \ -e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \ -e POSTGRES_USER=radarr \
-p 5432:5432/tcp \ -p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14 postgres:14
displayName: Start postgres displayName: Start postgres
- bash: | - bash: |
@@ -687,7 +687,7 @@ stages:
Radarr__Postgres__Password: 'radarr' Radarr__Postgres__Password: 'radarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
@@ -722,6 +722,7 @@ stages:
-e POSTGRES_PASSWORD=radarr \ -e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \ -e POSTGRES_USER=radarr \
-p 5432:5432/tcp \ -p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14 postgres:14
displayName: Start postgres displayName: Start postgres
- bash: | - bash: |
@@ -976,7 +977,6 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --lint - bash: ./build.sh --lint

View File

@@ -66,7 +66,8 @@ class CollectionFooter extends Component {
monitor, monitor,
monitored, monitored,
qualityProfileId, qualityProfileId,
minimumAvailability minimumAvailability,
rootFolderPath
} = this.state; } = this.state;
const changes = {}; const changes = {};
@@ -87,6 +88,10 @@ class CollectionFooter extends Component {
changes.minimumAvailability = minimumAvailability; changes.minimumAvailability = minimumAvailability;
} }
if (rootFolderPath !== NO_CHANGE) {
changes.rootFolderPath = rootFolderPath;
}
this.props.onUpdateSelectedPress(changes); this.props.onUpdateSelectedPress(changes);
}; };

View File

@@ -561,7 +561,7 @@ export const actionHandlers = handleThunks({
}, []); }, []);
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/movie/import', url: '/importlist/movie',
method: 'POST', method: 'POST',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify(allNewMovies) data: JSON.stringify(allNewMovies)

View File

@@ -116,7 +116,7 @@ export const filterPredicates = {
const predicate = filterTypePredicates[type]; const predicate = filterTypePredicates[type];
const { collection } = item; const { collection } = item;
return predicate(collection ? collection.name : '', filterValue); return predicate(collection ? collection.title : '', filterValue);
}, },
originalLanguage: function(item, filterValue, type) { originalLanguage: function(item, filterValue, type) {

View File

@@ -1,10 +1,12 @@
import _ from 'lodash'; import _ from 'lodash';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-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 { createThunk, handleThunks } from 'Store/thunks';
import sortByName from 'Utilities/Array/sortByName';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import getNewMovie from 'Utilities/Movie/getNewMovie'; import getNewMovie from 'Utilities/Movie/getNewMovie';
import translate from 'Utilities/String/translate';
import { set, update, updateItem } from './baseActions'; import { set, update, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import createSaveProviderHandler from './Creators/createSaveProviderHandler'; 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: [ filterBuilderProps: [
{ {
name: 'title', name: 'title',
label: 'Title', label: translate('Title'),
type: filterBuilderTypes.STRING type: filterBuilderTypes.STRING
}, },
{ {
name: 'monitored', name: 'monitored',
label: 'Monitored', label: translate('Monitored'),
type: filterBuilderTypes.EXACT, type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL 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
} }
] ]
}; };

View File

@@ -165,11 +165,11 @@ export const actionHandlers = handleThunks({
requestData.quality = quality; requestData.quality = quality;
} }
if (releaseGroup) { if (releaseGroup !== undefined) {
requestData.releaseGroup = releaseGroup; requestData.releaseGroup = releaseGroup;
} }
if (edition) { if (edition !== undefined) {
requestData.edition = edition; requestData.edition = edition;
} }
@@ -201,11 +201,11 @@ export const actionHandlers = handleThunks({
props.quality = quality; props.quality = quality;
} }
if (edition) { if (edition !== undefined) {
props.edition = edition; props.edition = edition;
} }
if (releaseGroup) { if (releaseGroup !== undefined) {
props.releaseGroup = releaseGroup; props.releaseGroup = releaseGroup;
} }

View File

@@ -341,8 +341,8 @@ export const defaultState = {
const collectionList = items.reduce((acc, movie) => { const collectionList = items.reduce((acc, movie) => {
if (movie.collection) { if (movie.collection) {
acc.push({ acc.push({
id: movie.collection.name, id: movie.collection.title,
name: movie.collection.name name: movie.collection.title
}); });
} }

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.1.0", "@fortawesome/free-regular-svg-icons": "6.1.0",
"@fortawesome/free-solid-svg-icons": "6.1.0", "@fortawesome/free-solid-svg-icons": "6.1.0",
"@fortawesome/react-fontawesome": "0.1.18", "@fortawesome/react-fontawesome": "0.1.18",
"@microsoft/signalr": "6.0.5", "@microsoft/signalr": "6.0.8",
"@sentry/browser": "6.18.2", "@sentry/browser": "6.18.2",
"@sentry/integrations": "6.18.2", "@sentry/integrations": "6.18.2",
"classnames": "2.3.1", "classnames": "2.3.1",

View File

@@ -90,7 +90,7 @@
<!-- Standard testing packages --> <!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'"> <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="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />

View File

@@ -8,5 +8,6 @@
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" /> <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="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="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> </packageSources>
</configuration> </configuration>

View File

@@ -20,7 +20,21 @@ namespace NzbDrone.Common.Test.InstrumentationTests
private static Exception[] FilteredExceptions = new Exception[] private static Exception[] FilteredExceptions = new Exception[]
{ {
new UnauthorizedAccessException() new UnauthorizedAccessException(),
new AggregateException(new Exception[]
{
new UnauthorizedAccessException(),
new UnauthorizedAccessException()
})
};
private static Exception[] NonFilteredExceptions = new Exception[]
{
new AggregateException(new Exception[]
{
new UnauthorizedAccessException(),
new NotImplementedException()
})
}; };
[SetUp] [SetUp]
@@ -63,6 +77,14 @@ namespace NzbDrone.Common.Test.InstrumentationTests
_subject.IsSentryMessage(log).Should().BeFalse(); _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] [Test]
[TestCaseSource("FilteredExceptions")] [TestCaseSource("FilteredExceptions")]
public void should_not_filter_event_for_filtered_exception_types_if_filtering_disabled(Exception ex) public void should_not_filter_event_for_filtered_exception_types_if_filtering_disabled(Exception ex)

View File

@@ -18,7 +18,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), 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(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), 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[^;=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory // 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"), new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),

View File

@@ -229,21 +229,48 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{ {
if (FilterEvents) if (FilterEvents)
{ {
var sqlEx = logEvent.Exception as SQLiteException; var exceptions = new List<Exception>();
if (sqlEx != null && FilteredSQLiteErrors.Contains(sqlEx.ResultCode))
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))) // The exception or aggregate exception children were not sentry exceptions
{ return false;
return false;
}
} }
return true; return true;

View File

@@ -10,10 +10,10 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" /> <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="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <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.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />

View File

@@ -27,6 +27,20 @@ namespace NzbDrone.Core.Test.Datastore
Mocker.Resolve<IDatabase>().Vacuum(); 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] [Test]
public void get_version() public void get_version()
{ {

View File

@@ -11,6 +11,7 @@ using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Collections;
using NzbDrone.Core.Movies.Commands; using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Credits; using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@@ -52,6 +53,10 @@ namespace NzbDrone.Core.Test.MovieTests
Mocker.GetMock<IProvideMovieInfo>() Mocker.GetMock<IProvideMovieInfo>()
.Setup(s => s.GetMovieInfo(It.IsAny<int>())) .Setup(s => s.GetMovieInfo(It.IsAny<int>()))
.Callback<int>((i) => { throw new MovieNotFoundException(i); }); .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) private void GivenNewMovieInfo(MovieMetadata movie)

View File

@@ -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");
}
}
}

View File

@@ -1,4 +1,4 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Internal; using NUnit.Framework.Internal;
@@ -47,5 +47,25 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Subject.GetMovieFolder(_movie) Subject.GetMovieFolder(_movie)
.Should().Be($"Movie Title ({_movie.TmdbId})"); .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");
}
} }
} }

View File

@@ -342,6 +342,16 @@ namespace NzbDrone.Core.Test.ParserTests
result.Languages.Should().BeEquivalentTo(Language.Bengali); 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.en.sub")] [TestCase("Movie.Title.en.sub")]
[TestCase("Movie Title.eng.sub")] [TestCase("Movie Title.eng.sub")]
[TestCase("Movie.Title.eng.forced.sub")] [TestCase("Movie.Title.eng.forced.sub")]

View File

@@ -41,6 +41,14 @@ namespace NzbDrone.Core.Test.ParserTests
ParseAndVerifyQuality(title, Source.TELESYNC, proper, Resolution.R720p); 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("S07E23 .avi ", false)]
[TestCase("Movie Name S02E01 HDTV XviD 2HD", false)] [TestCase("Movie Name S02E01 HDTV XviD 2HD", false)]
[TestCase("Movie Name S05E11 PROPER HDTV XviD 2HD", true)] [TestCase("Movie Name S05E11 PROPER HDTV XviD 2HD", true)]

View File

@@ -100,6 +100,10 @@ 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("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("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("A Movie in the Name (1964) (1080p BluRay x265 r00t)", "r00t")]
[TestCase("Movie Title (2022) (2160p ATV WEB-DL Hybrid H265 DV HDR DDP Atmos 5.1 English - HONE)", "HONE")]
[TestCase("Movie Title (2009) (2160p PMTP WEB-DL Hybrid H265 DV HDR10+ DDP Atmos 5.1 English - HONE)", "HONE")]
[TestCase("Why.Cant.You.Use.Normal.Characters.2021.2160p.UHD.HDR10+.BluRay.TrueHD.Atmos.7.1.x265-ZØNEHD", "ZØNEHD")]
[TestCase("Movie.Should.Not.Use.Dots.2022.1080p.BluRay.x265.10bit.Tigole", "Tigole")]
public void should_parse_exception_release_group(string title, string expected) public void should_parse_exception_release_group(string title, string expected)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);

View 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");
}
}
}

View File

@@ -8,6 +8,7 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -151,5 +152,107 @@ namespace NzbDrone.Core.Test.RootFolderTests
unmappedFolders.Count.Should().BeGreaterThan(0); unmappedFolders.Count.Should().BeGreaterThan(0);
unmappedFolders.Should().NotContain(u => u.Name == subFolder); 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");
}
} }
} }

View File

@@ -1,4 +1,5 @@
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats namespace NzbDrone.Core.CustomFormats
{ {
@@ -18,6 +19,8 @@ namespace NzbDrone.Core.CustomFormats
return (ICustomFormatSpecification)MemberwiseClone(); return (ICustomFormatSpecification)MemberwiseClone();
} }
public abstract NzbDroneValidationResult Validate();
public bool IsSatisfiedBy(ParsedMovieInfo movieInfo) public bool IsSatisfiedBy(ParsedMovieInfo movieInfo)
{ {
var match = IsSatisfiedByWithoutNegate(movieInfo); var match = IsSatisfiedByWithoutNegate(movieInfo);

View File

@@ -1,4 +1,5 @@
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats namespace NzbDrone.Core.CustomFormats
{ {
@@ -11,6 +12,8 @@ namespace NzbDrone.Core.CustomFormats
bool Negate { get; set; } bool Negate { get; set; }
bool Required { get; set; } bool Required { get; set; }
NzbDroneValidationResult Validate();
ICustomFormatSpecification Clone(); ICustomFormatSpecification Clone();
bool IsSatisfiedBy(ParsedMovieInfo movieInfo); bool IsSatisfiedBy(ParsedMovieInfo movieInfo);
} }

View File

@@ -1,11 +1,31 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats 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 public class IndexerFlagSpecification : CustomFormatSpecificationBase
{ {
private static readonly IndexerFlagSpecificationValidator Validator = new IndexerFlagSpecificationValidator();
public override int Order => 4; public override int Order => 4;
public override string ImplementationName => "Indexer Flag"; public override string ImplementationName => "Indexer Flag";
@@ -17,5 +37,10 @@ namespace NzbDrone.Core.CustomFormats
var flags = movieInfo?.ExtraInfo?.GetValueOrDefault("IndexerFlags") as IndexerFlags?; var flags = movieInfo?.ExtraInfo?.GetValueOrDefault("IndexerFlags") as IndexerFlags?;
return flags?.HasFlag((IndexerFlags)Value) == true; return flags?.HasFlag((IndexerFlags)Value) == true;
} }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }

View File

@@ -1,11 +1,31 @@
using System.Linq;
using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats 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 public class LanguageSpecification : CustomFormatSpecificationBase
{ {
private static readonly LanguageSpecificationValidator Validator = new LanguageSpecificationValidator();
public override int Order => 3; public override int Order => 3;
public override string ImplementationName => "Language"; public override string ImplementationName => "Language";
@@ -14,10 +34,15 @@ namespace NzbDrone.Core.CustomFormats
protected override bool IsSatisfiedByWithoutNegate(ParsedMovieInfo movieInfo) 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)movieInfo.ExtraInfo["OriginalLanguage"]
: (Language)Value; : (Language)Value;
return movieInfo?.Languages?.Contains(comparedLanguage) ?? false; return movieInfo?.Languages?.Contains(comparedLanguage) ?? false;
} }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }

View File

@@ -1,11 +1,31 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats 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 public class QualityModifierSpecification : CustomFormatSpecificationBase
{ {
private static readonly QualityModifierSpecificationValidator Validator = new QualityModifierSpecificationValidator();
public override int Order => 7; public override int Order => 7;
public override string ImplementationName => "Quality Modifier"; public override string ImplementationName => "Quality Modifier";
@@ -16,5 +36,10 @@ namespace NzbDrone.Core.CustomFormats
{ {
return (movieInfo?.Quality?.Quality?.Modifier ?? (int)Modifier.NONE) == (Modifier)Value; return (movieInfo?.Quality?.Quality?.Modifier ?? (int)Modifier.NONE) == (Modifier)Value;
} }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }

View File

@@ -1,10 +1,23 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats 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 public abstract class RegexSpecificationBase : CustomFormatSpecificationBase
{ {
private static readonly RegexSpecificationBaseValidator Validator = new RegexSpecificationBaseValidator();
protected Regex _regex; protected Regex _regex;
protected string _raw; protected string _raw;
@@ -15,7 +28,11 @@ namespace NzbDrone.Core.CustomFormats
set set
{ {
_raw = value; _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); return _regex.IsMatch(compared);
} }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }

View File

@@ -1,11 +1,23 @@
using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats namespace NzbDrone.Core.CustomFormats
{ {
public class ResolutionSpecificationValidator : AbstractValidator<ResolutionSpecification>
{
public ResolutionSpecificationValidator()
{
RuleFor(c => c.Value).NotEmpty();
}
}
public class ResolutionSpecification : CustomFormatSpecificationBase public class ResolutionSpecification : CustomFormatSpecificationBase
{ {
private static readonly ResolutionSpecificationValidator Validator = new ResolutionSpecificationValidator();
public override int Order => 6; public override int Order => 6;
public override string ImplementationName => "Resolution"; public override string ImplementationName => "Resolution";
@@ -16,5 +28,10 @@ namespace NzbDrone.Core.CustomFormats
{ {
return (movieInfo?.Quality?.Quality?.Resolution ?? (int)Resolution.Unknown) == Value; return (movieInfo?.Quality?.Quality?.Resolution ?? (int)Resolution.Unknown) == Value;
} }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }

View File

@@ -1,11 +1,24 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats 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 public class SizeSpecification : CustomFormatSpecificationBase
{ {
private static readonly SizeSpecificationValidator Validator = new SizeSpecificationValidator();
public override int Order => 8; public override int Order => 8;
public override string ImplementationName => "Size"; public override string ImplementationName => "Size";
@@ -21,5 +34,10 @@ namespace NzbDrone.Core.CustomFormats
return size > Min.Gigabytes() && size <= Max.Gigabytes(); return size > Min.Gigabytes() && size <= Max.Gigabytes();
} }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }

View File

@@ -1,11 +1,23 @@
using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats namespace NzbDrone.Core.CustomFormats
{ {
public class SourceSpecificationValidator : AbstractValidator<SourceSpecification>
{
public SourceSpecificationValidator()
{
RuleFor(c => c.Value).NotEmpty();
}
}
public class SourceSpecification : CustomFormatSpecificationBase public class SourceSpecification : CustomFormatSpecificationBase
{ {
private static readonly SourceSpecificationValidator Validator = new SourceSpecificationValidator();
public override int Order => 5; public override int Order => 5;
public override string ImplementationName => "Source"; public override string ImplementationName => "Source";
@@ -16,5 +28,10 @@ namespace NzbDrone.Core.CustomFormats
{ {
return (movieInfo?.Quality?.Quality?.Source ?? (int)Source.UNKNOWN) == (Source)Value; return (movieInfo?.Quality?.Quality?.Source ?? (int)Source.UNKNOWN) == (Source)Value;
} }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }

View File

@@ -114,7 +114,7 @@ namespace NzbDrone.Core.Datastore.Migration
SortTitle = Parser.Parser.NormalizeTitle(collectionName), SortTitle = Parser.Parser.NormalizeTitle(collectionName),
Added = added, Added = added,
QualityProfileId = qualityProfileId, QualityProfileId = qualityProfileId,
RootFolderPath = rootFolderPath, RootFolderPath = rootFolderPath.TrimEnd('/', '\\', ' '),
SearchOnAdd = true, SearchOnAdd = true,
MinimumAvailability = minimumAvailability MinimumAvailability = minimumAvailability
}); });

View File

@@ -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();
}
}
}

View File

@@ -305,6 +305,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("trailer", "plugin://plugin.video.youtube/play/?video_id=" + movie.MovieMetadata.Value.YouTubeTrailerId));
details.Add(new XElement("watched", watched));
if (movieFile.MediaInfo != null) if (movieFile.MediaInfo != null)
{ {
var sceneName = movieFile.GetSceneOrFileName(); var sceneName = movieFile.GetSceneOrFileName();

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Core.ImportLists
private readonly IImportListStatusService _importListStatusService; private readonly IImportListStatusService _importListStatusService;
private readonly IImportListMovieService _listMovieService; private readonly IImportListMovieService _listMovieService;
private readonly ISearchForNewMovie _movieSearch; private readonly ISearchForNewMovie _movieSearch;
private readonly IProvideMovieInfo _movieInfoService;
private readonly IMovieMetadataService _movieMetadataService; private readonly IMovieMetadataService _movieMetadataService;
private readonly Logger _logger; private readonly Logger _logger;
@@ -30,6 +31,7 @@ namespace NzbDrone.Core.ImportLists
IImportListStatusService importListStatusService, IImportListStatusService importListStatusService,
IImportListMovieService listMovieService, IImportListMovieService listMovieService,
ISearchForNewMovie movieSearch, ISearchForNewMovie movieSearch,
IProvideMovieInfo movieInfoService,
IMovieMetadataService movieMetadataService, IMovieMetadataService movieMetadataService,
Logger logger) Logger logger)
{ {
@@ -37,6 +39,7 @@ namespace NzbDrone.Core.ImportLists
_importListStatusService = importListStatusService; _importListStatusService = importListStatusService;
_listMovieService = listMovieService; _listMovieService = listMovieService;
_movieSearch = movieSearch; _movieSearch = movieSearch;
_movieInfoService = movieInfoService;
_movieMetadataService = movieMetadataService; _movieMetadataService = movieMetadataService;
_logger = logger; _logger = logger;
} }
@@ -84,20 +87,10 @@ namespace NzbDrone.Core.ImportLists
if (!importListReports.AnyFailure) if (!importListReports.AnyFailure)
{ {
// TODO some opportunity to bulk map here if we had the tmdbIds var alreadyMapped = result.Movies.Where(x => importListReports.Movies.Any(r => r.TmdbId == x.TmdbId));
var listMovies = importListReports.Movies.Select(x => var listMovies = MapMovieReports(importListReports.Movies.Where(x => !result.Movies.Any(r => r.TmdbId == x.TmdbId)).ToList()).Where(x => x.TmdbId > 0).ToList();
{
// 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();
listMovies.AddRange(alreadyMapped);
listMovies = listMovies.DistinctBy(x => x.TmdbId).ToList(); listMovies = listMovies.DistinctBy(x => x.TmdbId).ToList();
listMovies.ForEach(m => m.ListId = importList.Definition.Id); listMovies.ForEach(m => m.ListId = importList.Definition.Id);
@@ -148,13 +141,10 @@ namespace NzbDrone.Core.ImportLists
if (!importListReports.AnyFailure) if (!importListReports.AnyFailure)
{ {
// TODO some opportunity to bulk map here if we had the tmdbIds var listMovies = MapMovieReports(importListReports.Movies).Where(x => x.TmdbId > 0).ToList();
var listMovies = importListReports.Movies.Select(x =>
{
return MapMovieReport(x);
}).Where(x => x.TmdbId > 0).ToList();
listMovies = listMovies.DistinctBy(x => x.TmdbId).ToList(); listMovies = listMovies.DistinctBy(x => x.TmdbId).ToList();
listMovies.ForEach(m => m.ListId = importList.Definition.Id);
result.Movies.AddRange(listMovies); result.Movies.AddRange(listMovies);
_listMovieService.SyncMoviesForList(listMovies, importList.Definition.Id); _listMovieService.SyncMoviesForList(listMovies, importList.Definition.Id);
@@ -173,21 +163,31 @@ namespace NzbDrone.Core.ImportLists
return result; 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; if (movieMeta != null)
mappedListMovie.MovieMetadataId = mappedMovie.Id; {
mappedListMovie.MovieMetadata = movieMeta;
mappedListMovie.MovieMetadataId = movieMeta.Id;
}
mappedListMovies.Add(mappedListMovie);
} }
return mappedListMovie; return mappedListMovies;
} }
} }
} }

View File

@@ -27,7 +27,13 @@ namespace NzbDrone.Core.ImportLists.Trakt.List
{ {
var link = string.Empty; 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}"; link += $"users/{Settings.Username.Trim()}/lists/{listName}/items/movies?limit={Settings.Limit}";
var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.Get, Settings.AccessToken)); var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.Get, Settings.AccessToken));

View File

@@ -1,4 +1,6 @@
using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.ImportLists.Trakt.Popular namespace NzbDrone.Core.ImportLists.Trakt.Popular
@@ -9,6 +11,24 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular
: base() : base()
{ {
RuleFor(c => c.TraktListType).NotNull(); 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");
} }
} }
@@ -23,5 +43,17 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular
[FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktPopularListType), HelpText = "Type of list you're seeking to import from")] [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; } 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; }
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
@@ -29,24 +28,6 @@ namespace NzbDrone.Core.ImportLists.Trakt
.WithMessage("Must authenticate with Trakt") .WithMessage("Must authenticate with Trakt")
.When(c => c.AccessToken.IsNotNullOrWhiteSpace() && c.RefreshToken.IsNotNullOrWhiteSpace()); .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 // Limit not smaller than 1 and not larger than 100
RuleFor(c => c.Limit) RuleFor(c => c.Limit)
.GreaterThan(0) .GreaterThan(0)
@@ -62,10 +43,6 @@ namespace NzbDrone.Core.ImportLists.Trakt
public TraktSettingsBase() public TraktSettingsBase()
{ {
SignIn = "startOAuth"; SignIn = "startOAuth";
Rating = "0-100";
Certification = "NR,G,PG,PG-13,R,NC-17";
Genres = "";
Years = "";
Limit = 100; Limit = 100;
} }
@@ -84,18 +61,6 @@ namespace NzbDrone.Core.ImportLists.Trakt
[FieldDefinition(0, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] [FieldDefinition(0, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AuthUser { get; set; } 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")] [FieldDefinition(5, Label = "Limit", HelpText = "Limit the number of movies to get")]
public int Limit { get; set; } public int Limit { get; set; }

View File

@@ -42,14 +42,15 @@ namespace NzbDrone.Core.IndexerSearch
foreach (var movieId in message.MovieIds) foreach (var movieId in message.MovieIds)
{ {
var movie = _movieService.GetMovie(movieId); 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); _logger.Debug("Movie {0} is not monitored, skipping search", movie.Title);
continue; continue;
} }
var decisions = _releaseSearchService.MovieSearch(movieId, false, false); var decisions = _releaseSearchService.MovieSearch(movieId, userInvokedSearch, false);
downloadedCount += _processDownloadDecisions.ProcessDecisions(decisions).Grabbed.Count; downloadedCount += _processDownloadDecisions.ProcessDecisions(decisions).Grabbed.Count;
} }

View File

@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers
{ {
if (InfoHashElementName.IsNotNullOrWhiteSpace()) if (InfoHashElementName.IsNotNullOrWhiteSpace())
{ {
return item.FindDecendants(InfoHashElementName).FirstOrDefault().Value; return item.FindDecendants(InfoHashElementName).FirstOrDefault()?.Value;
} }
var magnetUrl = GetMagnetUrl(item); var magnetUrl = GetMagnetUrl(item);
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Indexers
{ {
if (MagnetElementName.IsNotNullOrWhiteSpace()) if (MagnetElementName.IsNotNullOrWhiteSpace())
{ {
var magnetURL = item.FindDecendants(MagnetElementName).FirstOrDefault().Value; var magnetURL = item.FindDecendants(MagnetElementName).FirstOrDefault()?.Value;
if (magnetURL.IsNotNullOrWhiteSpace() && magnetURL.StartsWith("magnet:")) if (magnetURL.IsNotNullOrWhiteSpace() && magnetURL.StartsWith("magnet:"))
{ {
return magnetURL; return magnetURL;

View File

@@ -105,6 +105,7 @@ namespace NzbDrone.Core.Languages
public static Language Ukrainian => new Language(32, "Ukrainian"); public static Language Ukrainian => new Language(32, "Ukrainian");
public static Language Persian => new Language(33, "Persian"); public static Language Persian => new Language(33, "Persian");
public static Language Bengali => new Language(34, "Bengali"); public static Language Bengali => new Language(34, "Bengali");
public static Language Slovak => new Language(35, "Slovak");
public static Language Any => new Language(-1, "Any"); public static Language Any => new Language(-1, "Any");
public static Language Original => new Language(-2, "Original"); public static Language Original => new Language(-2, "Original");
@@ -149,6 +150,7 @@ namespace NzbDrone.Core.Languages
Ukrainian, Ukrainian,
Persian, Persian,
Bengali, Bengali,
Slovak,
Any, Any,
Original Original
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -256,7 +256,7 @@
"AlternativeTitle": "Alternative Titel", "AlternativeTitle": "Alternative Titel",
"AllMoviesHiddenDueToFilter": "Alle Filme sind wegen dem Filter ausgeblendet.", "AllMoviesHiddenDueToFilter": "Alle Filme sind wegen dem Filter ausgeblendet.",
"Age": "Alter", "Age": "Alter",
"AddNewMovie": "Neuer Film...", "AddNewMovie": "Neuen Film hinzufügen",
"AddList": "Liste 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", "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", "UnsavedChanges": "Ungespeicherte Änderungen",
@@ -640,7 +640,7 @@
"RemovingTag": "Tag entfernen", "RemovingTag": "Tag entfernen",
"ReleaseWillBeProcessedInterp": "Release wird verarbeitet {0}", "ReleaseWillBeProcessedInterp": "Release wird verarbeitet {0}",
"Queued": "Eingereiht", "Queued": "Eingereiht",
"QualityProfileDeleteConfirm": "Qualitätsprofil '{0}' wirklich löschen?", "QualityProfileDeleteConfirm": "Qualitätsprofil '{0}' wirklich löschen",
"Pending": "Ausstehend", "Pending": "Ausstehend",
"Paused": "Pausiert", "Paused": "Pausiert",
"NegateHelpText": "Wenn aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.", "NegateHelpText": "Wenn aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.",
@@ -1047,7 +1047,7 @@
"RemotePathMappingCheckFilesLocalWrongOSPath": "Downloader {0} meldet Dateien in {1}, aber dies ist kein valider {2} Pfad. Überprüfe die Downloader Einstellungen.", "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.", "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.", "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.", "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.", "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.", "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", "OriginalTitle": "Originaler Titel",
"OriginalLanguage": "Originale Sprache", "OriginalLanguage": "Originale Sprache",
"Database": "Datenbank", "Database": "Datenbank",
"AllCollectionsHiddenDueToFilter": "Alle Filme sind wegen dem Filter ausgeblendet.", "AllCollectionsHiddenDueToFilter": "Alle Filme sind auf Grund des Filters ausgeblendet.",
"Collections": "Sammlung", "Collections": "Sammlungen",
"MonitorMovies": "Film beobachten", "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 )", "RssSyncHelpText": "Intervall in Minuten. Zum deaktivieren auf 0 setzen ( Dies wird das automatische Release erfassen deaktivieren )",
"CollectionOptions": "Sammlung Optionen", "CollectionOptions": "Sammlung Optionen",
"ChooseImportMode": "Wähle eine Importmethode", "ChooseImportMode": "Wähle eine Importmethode",
@@ -1127,7 +1127,7 @@
"ScrollMovies": "Filme scrollen", "ScrollMovies": "Filme scrollen",
"ShowCollectionDetails": "Status der Sammlung anzeigen", "ShowCollectionDetails": "Status der Sammlung anzeigen",
"RefreshCollections": "Sammlungen aktualisieren", "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", "ShowOverview": "Übersicht anzeigen",
"ShowPosters": "Plakate anzeigen", "ShowPosters": "Plakate anzeigen",
"CollectionShowDetailsHelpText": "Status und Eigenschaften der Sammlung anzeigen", "CollectionShowDetailsHelpText": "Status und Eigenschaften der Sammlung anzeigen",
@@ -1141,5 +1141,6 @@
"OnMovieAddedHelpText": "Ausführen wenn ein Film hinzugefügt wurde", "OnMovieAddedHelpText": "Ausführen wenn ein Film hinzugefügt wurde",
"UnableToLoadCollections": "Sammlungen können nicht geladen werden", "UnableToLoadCollections": "Sammlungen können nicht geladen werden",
"InstanceName": "Instanzname", "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"
} }

View File

@@ -1027,6 +1027,7 @@
"Torrents": "Torrents", "Torrents": "Torrents",
"TorrentsDisabled": "Torrents Disabled", "TorrentsDisabled": "Torrents Disabled",
"TotalFileSize": "Total File Size", "TotalFileSize": "Total File Size",
"TotalMovies": "Total Movies",
"TotalSpace": "Total Space", "TotalSpace": "Total Space",
"Trace": "Trace", "Trace": "Trace",
"Trailer": "Trailer", "Trailer": "Trailer",

View File

@@ -1119,5 +1119,6 @@
"ChooseImportMode": "Elegir Modo de Importación", "ChooseImportMode": "Elegir Modo de Importación",
"CollectionOptions": "Opciones de la Colección", "CollectionOptions": "Opciones de la Colección",
"CollectionShowDetailsHelpText": "Mostrar el estado y propiedades 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"
} }

View File

@@ -220,7 +220,7 @@
"MoveFiles": "Siirrä tiedostoja", "MoveFiles": "Siirrä tiedostoja",
"MovieFiles": "Elokuvatiedostot", "MovieFiles": "Elokuvatiedostot",
"MovieIsRecommend": "Elokuvaa suositellaan viimeaikaisen lisäyksen perusteella", "MovieIsRecommend": "Elokuvaa suositellaan viimeaikaisen lisäyksen perusteella",
"NextExecution": "Seuraava toteutus", "NextExecution": "Seuraava suoritus",
"NoAltTitle": "Ei vaihtoehtoisia nimikkeitä.", "NoAltTitle": "Ei vaihtoehtoisia nimikkeitä.",
"NoEventsFound": "Tapahtumia ei löytynyt", "NoEventsFound": "Tapahtumia ei löytynyt",
"NoLinks": "Ei linkkejä", "NoLinks": "Ei linkkejä",
@@ -320,8 +320,8 @@
"ImportErrors": "Tuontivirheet", "ImportErrors": "Tuontivirheet",
"Imported": "Tuodut", "Imported": "Tuodut",
"InvalidFormat": "Väärä muoto", "InvalidFormat": "Väärä muoto",
"LastDuration": "Viimeisin kesto", "LastDuration": "Edellinen kesto",
"LastExecution": "Viimeinen toteutus", "LastExecution": "Edellinen suoritus",
"ListSyncLevelHelpTextWarning": "Elokuvatiedostot poistetaan pysyvästi, mikä voi johtaa kirjaston pyyhkimiseen, jos luettelosi ovat tyhjät", "ListSyncLevelHelpTextWarning": "Elokuvatiedostot poistetaan pysyvästi, mikä voi johtaa kirjaston pyyhkimiseen, jos luettelosi ovat tyhjät",
"ListExclusions": "Listojen poikkeussäännöt", "ListExclusions": "Listojen poikkeussäännöt",
"Indexer": "Tietolähde", "Indexer": "Tietolähde",
@@ -1142,5 +1142,6 @@
"CollectionOptions": "Kokoelmien valinnat", "CollectionOptions": "Kokoelmien valinnat",
"CollectionShowDetailsHelpText": "Näytä kokoelmien tila ja ominaisuudet", "CollectionShowDetailsHelpText": "Näytä kokoelmien tila ja ominaisuudet",
"CollectionShowPostersHelpText": "Näytä kokoelmien julisteet", "CollectionShowPostersHelpText": "Näytä kokoelmien julisteet",
"RottenTomatoesRating": "Tomaattiarvio" "RottenTomatoesRating": "Tomaattiarvio",
"TotalMovies": "Elokuvia yhteensä"
} }

View File

@@ -106,7 +106,7 @@
"RemotePathMappings": "Mappages de chemins distants", "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", "ReleaseBranchCheckOfficialBranchMessage": "La branche {0} n'est pas une branche de version Radarr valide, vous ne recevrez pas de mises à jour",
"RefreshAndScan": "Actualiser et analyser", "RefreshAndScan": "Actualiser et analyser",
"Refresh": "Rafraîchir", "Refresh": "Actualiser",
"Ratings": "Évaluations", "Ratings": "Évaluations",
"Queue": "File d'attente", "Queue": "File d'attente",
"QualitySettingsSummary": "Tailles qualité et dénomination", "QualitySettingsSummary": "Tailles qualité et dénomination",
@@ -318,7 +318,7 @@
"ChangeHasNotBeenSavedYet": "Les changements n'ont pas encore été sauvegardés", "ChangeHasNotBeenSavedYet": "Les changements n'ont pas encore été sauvegardés",
"ChangeFileDate": "Changer la date du fichier", "ChangeFileDate": "Changer la date du fichier",
"CertificationCountryHelpText": "Choisir un pays pour les classifications de films", "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", "CertificateValidation": "Validation du certificat",
"BypassProxyForLocalAddresses": "Contourner le proxy pour les adresses locales", "BypassProxyForLocalAddresses": "Contourner le proxy pour les adresses locales",
"Branch": "Branche", "Branch": "Branche",
@@ -1114,10 +1114,28 @@
"SetReleaseGroup": "Régler le groupe de publication", "SetReleaseGroup": "Régler le groupe de publication",
"RefreshMonitoredIntervalHelpText": "Intervalle en minutes entre chaque vérification des téléchargements, minimum 1 minute", "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é.", "AllCollectionsHiddenDueToFilter": "Tous les films sont masqués en raison du filtre appliqué.",
"Collections": "Collection", "Collections": "Collections",
"MonitorMovies": "Surveiller le film", "MonitorMovies": "Surveiller le film",
"NoCollections": "Aucun film trouvé, pour commencer, vous voudrez ajouter un nouveau film ou importer des films existants.", "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)", "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)", "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"
} }

View File

@@ -21,7 +21,7 @@
"AddExclusion": "Kivétel hozzáadása", "AddExclusion": "Kivétel hozzáadása",
"Activity": "Aktivitás", "Activity": "Aktivitás",
"Error": "Hiba", "Error": "Hiba",
"Ended": "Befejezve", "Ended": "Vége lett",
"EnableCompletedDownloadHandlingHelpText": "A befejezett letöltések automatikus importálása a letöltési kliensből", "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", "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ó", "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.", "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.", "RadarrSupportsAnyDownloadClient": "A Radarr számos népszerű torrent és usenet letöltési ügyfelet támogat.",
"QuickImport": "Automatikus Áthelyezés", "QuickImport": "Automatikus Áthelyezés",
"Queued": "Sorban", "Queued": "Sorba helyezve",
"Queue": "Várakozási sor", "Queue": "Várakozási sor",
"QualitySettingsSummary": "Minőségi méretek és elnevezés", "QualitySettingsSummary": "Minőségi méretek és elnevezés",
"QualitySettings": "Minőségi beállítások", "QualitySettings": "Minőségi beállítások",
@@ -616,7 +616,7 @@
"Level": "Szint", "Level": "Szint",
"LaunchBrowserHelpText": " Nyisson meg egy böngészőt, és az alkalmazás indításakor lépjen a Radarr kezdőlapjára.", "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ő", "LastWriteTime": "Utolsó írási idő",
"LastDuration": "Utolsó Hossza", "LastDuration": "Utolsó időtartam",
"Languages": "Nyelvek", "Languages": "Nyelvek",
"LanguageHelpText": "Nyelv a Releasekhez", "LanguageHelpText": "Nyelv a Releasekhez",
"Language": "Nyelv", "Language": "Nyelv",
@@ -896,7 +896,7 @@
"HomePage": "Kezdőlap", "HomePage": "Kezdőlap",
"LogOnly": "Csak naplózás", "LogOnly": "Csak naplózás",
"LastUsed": "Utoljára használt", "LastUsed": "Utoljára használt",
"LastExecution": "Utoljára végrehajtott", "LastExecution": "Utol végrehajtás",
"Large": "Óriási", "Large": "Óriási",
"KeepAndUnmonitorMovie": "Megtartás és megfigyelés kikapcsolása a filmnél", "KeepAndUnmonitorMovie": "Megtartás és megfigyelés kikapcsolása a filmnél",
"InvalidFormat": "Érvénytelen Formátum", "InvalidFormat": "Érvénytelen Formátum",
@@ -992,7 +992,7 @@
"NoLinks": "Nincsenek Linkek", "NoLinks": "Nincsenek Linkek",
"NoEventsFound": "Nem található esemény", "NoEventsFound": "Nem található esemény",
"NoAltTitle": "Nincs alternatív cím.", "NoAltTitle": "Nincs alternatív cím.",
"NextExecution": "Következő futtatás", "NextExecution": "Következő végrehajtás",
"Negated": "Megtagadva", "Negated": "Megtagadva",
"Negate": "Megtagadás", "Negate": "Megtagadás",
"MultiLanguage": "Többnyelvű", "MultiLanguage": "Többnyelvű",
@@ -1105,7 +1105,7 @@
"Never": "Soha", "Never": "Soha",
"Rating": "Értékelés", "Rating": "Értékelés",
"SetReleaseGroup": "Kiadási csoport beállítása", "SetReleaseGroup": "Kiadási csoport beállítása",
"Started": "Elindult", "Started": "Elkezdődött",
"Waiting": "Várakozás", "Waiting": "Várakozás",
"OnApplicationUpdateHelpText": "Alkalmazásfrissítésről", "OnApplicationUpdateHelpText": "Alkalmazásfrissítésről",
"SizeLimit": "Méretkorlát", "SizeLimit": "Méretkorlát",
@@ -1142,5 +1142,6 @@
"OnMovieAddedHelpText": "Film hozzáadásakor", "OnMovieAddedHelpText": "Film hozzáadásakor",
"ShowPosters": "Poszterek megjelenítése", "ShowPosters": "Poszterek megjelenítése",
"InstanceNameHelpText": "Példánynév a böngésző lapon és a syslog alkalmazás neve", "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"
} }

View File

@@ -254,5 +254,6 @@
"Language": "språk", "Language": "språk",
"List": "Liste", "List": "Liste",
"New": "Ny", "New": "Ny",
"RemotePathMappings": "Ekstern portmapping" "RemotePathMappings": "Ekstern portmapping",
"Languages": "språk"
} }

View File

@@ -1062,7 +1062,7 @@
"Collections": "Kolekcje", "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 zwolnień)",
"NoCollections": "Nie znaleziono żadnych filmów. Aby rozpocząć, musisz dodać nowy film lub zaimportować istniejące", "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ą", "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.", "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", "NotificationTriggersHelpText": "Wybierz zdarzenia, które mają uruchamiać to powiadomienie",
@@ -1078,7 +1078,7 @@
"ManualImportSetReleaseGroup": "Import ręczny - podaj nazwę grupy", "ManualImportSetReleaseGroup": "Import ręczny - podaj nazwę grupy",
"Never": "Nigdy", "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.", "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. Być może należy zmienić uprawnienia dostępu do folderu/ścieżkę dostępu.",
"RemoveFailed": "Usuń nieudane", "RemoveFailed": "Usuń nieudane",
"RemoveDownloadsAlert": "Ustawienia usuwania zostały przeniesione do ustawień poszczególnych klientów pobierania powyżej.", "RemoveDownloadsAlert": "Ustawienia usuwania zostały przeniesione do ustawień poszczególnych klientów pobierania powyżej.",
"ShowCollectionDetails": "Pokaż stan kolekcji", "ShowCollectionDetails": "Pokaż stan kolekcji",
@@ -1141,5 +1141,7 @@
"CollectionShowPostersHelpText": "Pokaż plakaty elementów kolekcji", "CollectionShowPostersHelpText": "Pokaż plakaty elementów kolekcji",
"CollectionOptions": "Opcje Kolekcji", "CollectionOptions": "Opcje Kolekcji",
"CollectionShowDetailsHelpText": "Pokaż status i właściwości 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"
} }

View File

@@ -1,5 +1,5 @@
{ {
"LastExecution": "Execução mais recente", "LastExecution": "Última Execução",
"Large": "Grande", "Large": "Grande",
"Languages": "Idiomas", "Languages": "Idiomas",
"LanguageHelpText": "Idioma das versões", "LanguageHelpText": "Idioma das versões",
@@ -19,7 +19,7 @@
"IndexerStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a falhas", "IndexerStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a falhas",
"IndexersSettingsSummary": "Indexadores e restrições de lançamento", "IndexersSettingsSummary": "Indexadores e restrições de lançamento",
"IndexerSettings": "Configurações do indexador", "IndexerSettings": "Configurações do indexador",
"IndexerSearchCheckNoInteractiveMessage": "Nenhum indexador disponível com a Pesquisa Interativa ativada, o Radarr não fornecerá nenhum resultado de pesquisa interativa", "IndexerSearchCheckNoInteractiveMessage": "Nenhum indexador disponível com a Pesquisa interativa ativada, o Radarr não fornecerá nenhum resultado de pesquisa interativo",
"IndexerSearchCheckNoAvailableIndexersMessage": "Todos os indexadores com capacidade de pesquisa estão temporariamente indisponíveis devido a erros recentes do indexador", "IndexerSearchCheckNoAvailableIndexersMessage": "Todos os indexadores com capacidade de pesquisa estão temporariamente indisponíveis devido a erros recentes do indexador",
"IndexerSearchCheckNoAutomaticMessage": "Nenhum indexador disponível com a Pesquisa automática habilitada, o Radarr não fornecerá nenhum resultado de pesquisa automática", "IndexerSearchCheckNoAutomaticMessage": "Nenhum indexador disponível com a Pesquisa automática habilitada, o Radarr não fornecerá nenhum resultado de pesquisa automática",
"Indexers": "Indexadores", "Indexers": "Indexadores",
@@ -571,7 +571,7 @@
"NoChange": "Sem alteração", "NoChange": "Sem alteração",
"NoBackupsAreAvailable": "Não há backups disponíveis", "NoBackupsAreAvailable": "Não há backups disponíveis",
"NoAltTitle": "Nenhum título alternativo.", "NoAltTitle": "Nenhum título alternativo.",
"NextExecution": "Próxima execução", "NextExecution": "Próxima Execução",
"New": "Novo", "New": "Novo",
"NetCore": ".NET", "NetCore": ".NET",
"ShowAsAllDayEvents": "Mostrar como eventos de dia inteiro", "ShowAsAllDayEvents": "Mostrar como eventos de dia inteiro",
@@ -668,7 +668,7 @@
"SomeResultsHiddenFilter": "Alguns resultados estão ocultos pelo filtro aplicado", "SomeResultsHiddenFilter": "Alguns resultados estão ocultos pelo filtro aplicado",
"Socks5": "Socks5 (suporte ao TOR)", "Socks5": "Socks5 (suporte ao TOR)",
"Small": "Pequeno", "Small": "Pequeno",
"SkipFreeSpaceCheckWhenImportingHelpText": "Usar o Radarr quando for impossível detectar o espaço livre da sua pasta raiz do filme", "SkipFreeSpaceCheckWhenImportingHelpText": "Use quando o Radarr não conseguir detectar espaço livre na pasta raiz do filme",
"SkipFreeSpaceCheck": "Ignorar verificação de espaço livre", "SkipFreeSpaceCheck": "Ignorar verificação de espaço livre",
"SizeOnDisk": "Tamanho em disco", "SizeOnDisk": "Tamanho em disco",
"Size": "Tamanho", "Size": "Tamanho",
@@ -961,7 +961,7 @@
"UpdateSelected": "Atualizar selecionado(s)", "UpdateSelected": "Atualizar selecionado(s)",
"UpdateScriptPathHelpText": "Caminho para um script personalizado que usa um pacote de atualização extraído e lida com o restante do processo de atualização", "UpdateScriptPathHelpText": "Caminho para um script personalizado que usa um pacote de atualização extraído e lida com o restante do processo de atualização",
"Updates": "Atualizações", "Updates": "Atualizações",
"UpdateMechanismHelpText": "Usar atualizador integrado do Radarr ou um script", "UpdateMechanismHelpText": "Use o atualizador integrado do Radarr ou um script",
"UpdateCheckUINotWritableMessage": "Não é possível instalar a atualização porque a pasta de interface do usuário '{0}' não é gravável pelo usuário '{1}'.", "UpdateCheckUINotWritableMessage": "Não é possível instalar a atualização porque a pasta de interface do usuário '{0}' não é gravável pelo usuário '{1}'.",
"UpdateCheckStartupTranslocationMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' está em uma pasta App Translocation.", "UpdateCheckStartupTranslocationMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' está em uma pasta App Translocation.",
"UpdateCheckStartupNotWritableMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' não pode ser gravada pelo usuário '{1}'.", "UpdateCheckStartupNotWritableMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' não pode ser gravada pelo usuário '{1}'.",
@@ -1053,7 +1053,7 @@
"RemotePathMappingCheckImportFailed": "O Radarr não conseguiu importar um filme. Verifique os logs para saber mais.", "RemotePathMappingCheckImportFailed": "O Radarr não conseguiu importar um filme. Verifique os logs para saber mais.",
"RemotePathMappingCheckFileRemoved": "O arquivo {0} foi removido no meio do processamento.", "RemotePathMappingCheckFileRemoved": "O arquivo {0} foi removido no meio do processamento.",
"RemotePathMappingCheckDownloadPermissions": "O Radarr pode ver, mas não pode acessar o filme baixado {0}. Provável erro de permissões.", "RemotePathMappingCheckDownloadPermissions": "O Radarr pode ver, mas não pode acessar o filme baixado {0}. Provável erro de permissões.",
"RemotePathMappingCheckGenericPermissions": "O cliente para download {0} põe os downloads em {1}, mas o Radarr não pode ver esse diretório. Você pode precisar ajustar as permissões da pasta.", "RemotePathMappingCheckGenericPermissions": "O cliente de download {0} coloca downloads em {1} mas o Radarr não pode ver este diretório. Pode ser necessário ajustar as permissões da pasta.",
"RemotePathMappingCheckWrongOSPath": "O cliente de download remoto {0} coloca downloads em {1}, mas este não é um caminho {2} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", "RemotePathMappingCheckWrongOSPath": "O cliente de download remoto {0} coloca downloads em {1}, mas este não é um caminho {2} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.",
"RemotePathMappingCheckLocalWrongOSPath": "O cliente de download local {0} coloca downloads em {1}, mas este não é um caminho {2} válido. Revise as configurações do seu cliente de download.", "RemotePathMappingCheckLocalWrongOSPath": "O cliente de download local {0} coloca downloads em {1}, mas este não é um caminho {2} válido. Revise as configurações do seu cliente de download.",
"RemotePathMappingCheckLocalFolderMissing": "O cliente de download remoto {0} coloca downloads em {1}, mas esse diretório parece não existir. Mapeamento de caminho remoto provavelmente ausente ou incorreto.", "RemotePathMappingCheckLocalFolderMissing": "O cliente de download remoto {0} coloca downloads em {1}, mas esse diretório parece não existir. Mapeamento de caminho remoto provavelmente ausente ou incorreto.",
@@ -1142,5 +1142,6 @@
"CollectionShowDetailsHelpText": "Mostrar estado e propriedades da coleção", "CollectionShowDetailsHelpText": "Mostrar estado e propriedades da coleção",
"CollectionShowOverviewsHelpText": "Mostrar visão geral da coleção", "CollectionShowOverviewsHelpText": "Mostrar visão geral da coleção",
"CollectionShowPostersHelpText": "Mostrar pôsteres de itens da coleção", "CollectionShowPostersHelpText": "Mostrar pôsteres de itens da coleção",
"RottenTomatoesRating": "Avaliação Tomato" "RottenTomatoesRating": "Avaliação Tomato",
"TotalMovies": "Total de Filmes"
} }

View File

@@ -637,7 +637,7 @@
"InstallLatest": "安装最新版", "InstallLatest": "安装最新版",
"IndexerStatusCheckSingleClientMessage": "搜刮器因错误不可用:{0}", "IndexerStatusCheckSingleClientMessage": "搜刮器因错误不可用:{0}",
"IndexerStatusCheckAllClientMessage": "所有搜刮器都因错误不可用", "IndexerStatusCheckAllClientMessage": "所有搜刮器都因错误不可用",
"IndexerSearchCheckNoInteractiveMessage": "没有任何索引器开启了手动搜索Radarr 不会提供任何手动搜索结果", "IndexerSearchCheckNoInteractiveMessage": "没有任何索引器开启了手动搜索Radarr 不会提供任何手动搜索结果",
"IndexerRssHealthCheckNoIndexers": "没有任何索引器开启了RSS同步Radarr不会自动抓取新发布的影片", "IndexerRssHealthCheckNoIndexers": "没有任何索引器开启了RSS同步Radarr不会自动抓取新发布的影片",
"IndexerLongTermStatusCheckSingleClientMessage": "由于故障6小时下列索引器都已不可用{0}", "IndexerLongTermStatusCheckSingleClientMessage": "由于故障6小时下列索引器都已不可用{0}",
"IndexerLongTermStatusCheckAllClientMessage": "由于故障超过6小时所有索引器均不可用", "IndexerLongTermStatusCheckAllClientMessage": "由于故障超过6小时所有索引器均不可用",
@@ -740,7 +740,7 @@
"UnableToAddANewNotificationPleaseTryAgain": "无法添加新通知,请稍后重试。", "UnableToAddANewNotificationPleaseTryAgain": "无法添加新通知,请稍后重试。",
"URLBase": "基本URL", "URLBase": "基本URL",
"RemovedMovieCheckMultipleMessage": "电影“{0}”已从TMDb移除", "RemovedMovieCheckMultipleMessage": "电影“{0}”已从TMDb移除",
"SkipFreeSpaceCheckWhenImportingHelpText": "当 Radarr 无法从您的电影根文件夹中检测到空闲空间时使用", "SkipFreeSpaceCheckWhenImportingHelpText": "当 Radarr 无法从movie根目录检测到空间时使用",
"UnableToAddANewQualityProfilePleaseTryAgain": "无法添加新影片质量配置,请稍后重试。", "UnableToAddANewQualityProfilePleaseTryAgain": "无法添加新影片质量配置,请稍后重试。",
"ThisCannotBeCancelled": "在不禁用所有索引器的情况下,一旦启动就无法取消。", "ThisCannotBeCancelled": "在不禁用所有索引器的情况下,一旦启动就无法取消。",
"SearchCutoffUnmet": "搜索未满足终止条件的", "SearchCutoffUnmet": "搜索未满足终止条件的",
@@ -1135,5 +1135,12 @@
"NoCollections": "没有发现集合,点击添加新的电影或者导入已经存在的电影", "NoCollections": "没有发现集合,点击添加新的电影或者导入已经存在的电影",
"InstanceNameHelpText": "选项卡及日志应用名称", "InstanceNameHelpText": "选项卡及日志应用名称",
"ScrollMovies": "滚动电影", "ScrollMovies": "滚动电影",
"CollectionOptions": "Collection Options" "CollectionOptions": "集选项Collection Options",
"CollectionShowDetailsHelpText": "显示集合collection的状态和属性",
"CollectionShowOverviewsHelpText": "显示集合collection的概述",
"CollectionShowPostersHelpText": "显示集合项目海报",
"RottenTomatoesRating": "烂番茄指数",
"ShowPosters": "显示海报",
"OnMovieAdded": "电影添加时",
"OnMovieAddedHelpText": "电影添加时"
} }

View File

@@ -1,7 +1,14 @@
{ {
"About": "關於", "About": "關於",
"Add": "添加", "Add": "新增",
"AcceptConfirmationModal": "接受確認模式", "AcceptConfirmationModal": "接受確認模式",
"Actions": "‎行動‎", "Actions": "執行",
"Activity": "‎活動‎" "Activity": "‎活動‎",
"AddIndexer": "新增索引",
"AddingTag": "新增標籤",
"AddDownloadClient": "新增下載器",
"Added": "以新增",
"Age": "年齡",
"All": "全部",
"Analytics": "分析"
} }

View File

@@ -5,6 +5,7 @@ using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
@@ -260,6 +261,20 @@ namespace NzbDrone.Core.MediaFiles
}; };
} }
var extension = Path.GetExtension(fileInfo.Name);
if (extension.IsNullOrWhiteSpace() || !MediaFileExtensions.Extensions.Contains(extension))
{
_logger.Debug("[{0}] has an unsupported extension: '{1}'", fileInfo.FullName, extension);
return new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalMovie { Path = fileInfo.FullName },
new Rejection($"Invalid video file, unsupported extension: '{extension}'")),
$"Invalid video file, unsupported extension: '{extension}'")
};
}
if (downloadClientItem == null) if (downloadClientItem == null)
{ {
if (_diskProvider.IsFileLocked(fileInfo.FullName)) if (_diskProvider.IsFileLocked(fileInfo.FullName))

View File

@@ -327,6 +327,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
} }
else if (movie.ImdbId.IsNotNullOrWhiteSpace()) else if (movie.ImdbId.IsNotNullOrWhiteSpace())
{ {
newMovie = _movieMetadataService.FindByImdbId(Parser.Parser.NormalizeImdbId(movie.ImdbId));
if (newMovie != null)
{
return newMovie;
}
newMovie = GetMovieByImdbId(movie.ImdbId); newMovie = GetMovieByImdbId(movie.ImdbId);
} }
else else
@@ -337,7 +344,16 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
yearStr = $" {movie.Year}"; yearStr = $" {movie.Year}";
} }
newMovie = SearchForNewMovie(movie.Title + yearStr).FirstOrDefault().MovieMetadata; var newMovieObject = SearchForNewMovie(movie.Title + yearStr).FirstOrDefault();
if (newMovieObject == null)
{
newMovie = null;
}
else
{
newMovie = newMovieObject.MovieMetadata;
}
} }
if (newMovie == null) if (newMovie == null)

View File

@@ -122,7 +122,7 @@ namespace NzbDrone.Core.Movies.Collections
var collection = FindByTmdbId(collectionTmdbId); var collection = FindByTmdbId(collectionTmdbId);
_repo.Delete(collectionTmdbId); _repo.Delete(collection.Id);
_eventAggregator.PublishEvent(new CollectionDeletedEvent(collection)); _eventAggregator.PublishEvent(new CollectionDeletedEvent(collection));
} }

View File

@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Movies
_diskProvider.CreateFolder(new DirectoryInfo(destinationPath).Parent.FullName); _diskProvider.CreateFolder(new DirectoryInfo(destinationPath).Parent.FullName);
_diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move); _diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move);
_logger.ProgressInfo("{0} moved successfully to {1}", movie.Title, movie.Path); _logger.ProgressInfo("{0} moved successfully to {1}", movie.Title, destinationPath);
_eventAggregator.PublishEvent(new MovieMovedEvent(movie, sourcePath, destinationPath)); _eventAggregator.PublishEvent(new MovieMovedEvent(movie, sourcePath, destinationPath));
} }

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.Movies
public interface IMovieMetadataRepository : IBasicRepository<MovieMetadata> public interface IMovieMetadataRepository : IBasicRepository<MovieMetadata>
{ {
MovieMetadata FindByTmdbId(int tmdbId); MovieMetadata FindByTmdbId(int tmdbId);
MovieMetadata FindByImdbId(string imdbId);
List<MovieMetadata> FindById(List<int> tmdbIds); List<MovieMetadata> FindById(List<int> tmdbIds);
List<MovieMetadata> GetMoviesWithCollections(); List<MovieMetadata> GetMoviesWithCollections();
List<MovieMetadata> GetMoviesByCollectionTmdbId(int collectionId); List<MovieMetadata> GetMoviesByCollectionTmdbId(int collectionId);
@@ -27,9 +28,14 @@ namespace NzbDrone.Core.Movies
_logger = logger; _logger = logger;
} }
public MovieMetadata FindByTmdbId(int tmdbid) public MovieMetadata FindByTmdbId(int tmdbId)
{ {
return Query(x => x.TmdbId == tmdbid).FirstOrDefault(); return Query(x => x.TmdbId == tmdbId).FirstOrDefault();
}
public MovieMetadata FindByImdbId(string imdbId)
{
return Query(x => x.ImdbId == imdbId).FirstOrDefault();
} }
public List<MovieMetadata> FindById(List<int> tmdbIds) public List<MovieMetadata> FindById(List<int> tmdbIds)

View File

@@ -5,7 +5,8 @@ namespace NzbDrone.Core.Movies
public interface IMovieMetadataService public interface IMovieMetadataService
{ {
MovieMetadata Get(int id); MovieMetadata Get(int id);
MovieMetadata FindByTmdbId(int tmdbid); MovieMetadata FindByTmdbId(int tmdbId);
MovieMetadata FindByImdbId(string imdbId);
List<MovieMetadata> GetMoviesWithCollections(); List<MovieMetadata> GetMoviesWithCollections();
List<MovieMetadata> GetMoviesByCollectionTmdbId(int collectionId); List<MovieMetadata> GetMoviesByCollectionTmdbId(int collectionId);
bool Upsert(MovieMetadata movie); bool Upsert(MovieMetadata movie);
@@ -21,9 +22,14 @@ namespace NzbDrone.Core.Movies
_movieMetadataRepository = movieMetadataRepository; _movieMetadataRepository = movieMetadataRepository;
} }
public MovieMetadata FindByTmdbId(int tmdbid) public MovieMetadata FindByTmdbId(int tmdbId)
{ {
return _movieMetadataRepository.FindByTmdbId(tmdbid); return _movieMetadataRepository.FindByTmdbId(tmdbId);
}
public MovieMetadata FindByImdbId(string imdbId)
{
return _movieMetadataRepository.FindByImdbId(imdbId);
} }
public List<MovieMetadata> GetMoviesWithCollections() public List<MovieMetadata> GetMoviesWithCollections()

View File

@@ -119,10 +119,18 @@ namespace NzbDrone.Core.Movies
return FindByTitle(new List<string> { title }, year, otherTitles, candidates); return FindByTitle(new List<string> { title }, year, otherTitles, candidates);
} }
public Movie FindByTitle(List<string> cleanTitles, int? year, List<string> otherTitles, List<Movie> candidates) public Movie FindByTitle(List<string> titles, int? year, List<string> otherTitles, List<Movie> candidates)
{ {
var cleanTitles = titles.Select(t => t.CleanMovieTitle().ToLowerInvariant());
var result = candidates.Where(x => cleanTitles.Contains(x.MovieMetadata.Value.CleanTitle)).FirstWithYear(year); var result = candidates.Where(x => cleanTitles.Contains(x.MovieMetadata.Value.CleanTitle)).FirstWithYear(year);
if (result == null)
{
result =
candidates.Where(movie => cleanTitles.Contains(movie.MovieMetadata.Value.CleanOriginalTitle)).FirstWithYear(year);
}
if (result == null) if (result == null)
{ {
result = result =

View File

@@ -3,12 +3,11 @@ using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.ImportLists.ImportExclusions;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies.Collections; using NzbDrone.Core.Movies.Collections;
using NzbDrone.Core.Movies.Commands; using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Events;
namespace NzbDrone.Core.Movies namespace NzbDrone.Core.Movies
{ {
@@ -19,6 +18,7 @@ namespace NzbDrone.Core.Movies
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IMovieMetadataService _movieMetadataService; private readonly IMovieMetadataService _movieMetadataService;
private readonly IAddMovieService _addMovieService; private readonly IAddMovieService _addMovieService;
private readonly IImportExclusionsService _importExclusionService;
private readonly Logger _logger; private readonly Logger _logger;
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Movies
IMovieService movieService, IMovieService movieService,
IMovieMetadataService movieMetadataService, IMovieMetadataService movieMetadataService,
IAddMovieService addMovieService, IAddMovieService addMovieService,
IImportExclusionsService importExclusionsService,
Logger logger) Logger logger)
{ {
_movieInfo = movieInfo; _movieInfo = movieInfo;
@@ -34,6 +35,7 @@ namespace NzbDrone.Core.Movies
_movieService = movieService; _movieService = movieService;
_movieMetadataService = movieMetadataService; _movieMetadataService = movieMetadataService;
_addMovieService = addMovieService; _addMovieService = addMovieService;
_importExclusionService = importExclusionsService;
_logger = logger; _logger = logger;
} }
@@ -99,7 +101,8 @@ namespace NzbDrone.Core.Movies
{ {
var existingMovies = _movieService.AllMovieTmdbIds(); var existingMovies = _movieService.AllMovieTmdbIds();
var collectionMovies = _movieMetadataService.GetMoviesByCollectionTmdbId(collection.TmdbId); var collectionMovies = _movieMetadataService.GetMoviesByCollectionTmdbId(collection.TmdbId);
var moviesToAdd = collectionMovies.Where(m => !existingMovies.Contains(m.TmdbId)); var excludedMovies = _importExclusionService.GetAllExclusions().Select(e => e.TmdbId);
var moviesToAdd = collectionMovies.Where(m => !existingMovies.Contains(m.TmdbId)).Where(m => !excludedMovies.Contains(m.TmdbId));
if (moviesToAdd.Any()) if (moviesToAdd.Any())
{ {

View File

@@ -140,7 +140,7 @@ namespace NzbDrone.Core.Movies
SearchOnAdd = movie.AddOptions?.SearchForMovie ?? false, SearchOnAdd = movie.AddOptions?.SearchForMovie ?? false,
QualityProfileId = movie.ProfileId, QualityProfileId = movie.ProfileId,
MinimumAvailability = movie.MinimumAvailability, MinimumAvailability = movie.MinimumAvailability,
RootFolderPath = _folderService.GetBestRootFolderPath(movie.Path) RootFolderPath = _folderService.GetBestRootFolderPath(movie.Path).TrimEnd('/', '\\', ' ')
}); });
movieMetadata.CollectionTmdbId = newCollection.TmdbId; movieMetadata.CollectionTmdbId = newCollection.TmdbId;

View File

@@ -98,6 +98,14 @@ namespace NzbDrone.Core.Notifications.Discord
discordField.Name = "Links"; discordField.Name = "Links";
discordField.Value = GetLinksString(message.Movie); discordField.Value = GetLinksString(message.Movie);
break; break;
case DiscordGrabFieldType.CustomFormats:
discordField.Name = "Custom Formats";
discordField.Value = string.Join("|", message.RemoteMovie.CustomFormats);
break;
case DiscordGrabFieldType.CustomFormatScore:
discordField.Name = "Custom Format Score";
discordField.Value = message.RemoteMovie.CustomFormatScore.ToString();
break;
} }
if (discordField.Name.IsNotNullOrWhiteSpace() && discordField.Value.IsNotNullOrWhiteSpace()) if (discordField.Name.IsNotNullOrWhiteSpace() && discordField.Value.IsNotNullOrWhiteSpace())

View File

@@ -11,7 +11,9 @@ namespace NzbDrone.Core.Notifications.Discord
Links, Links,
Release, Release,
Poster, Poster,
Fanart Fanart,
CustomFormats,
CustomFormatScore
} }
public enum DiscordImportFieldType public enum DiscordImportFieldType

View File

@@ -48,6 +48,8 @@ namespace NzbDrone.Core.Notifications.Notifiarr
variables.Add("Radarr_Download_Client", message.DownloadClientName ?? string.Empty); variables.Add("Radarr_Download_Client", message.DownloadClientName ?? string.Empty);
variables.Add("Radarr_Download_Client_Type", message.DownloadClientType ?? string.Empty); variables.Add("Radarr_Download_Client_Type", message.DownloadClientType ?? string.Empty);
variables.Add("Radarr_Download_Id", message.DownloadId ?? string.Empty); variables.Add("Radarr_Download_Id", message.DownloadId ?? string.Empty);
variables.Add("Radarr_Release_CustomFormat", string.Join("|", remoteMovie.CustomFormats));
variables.Add("Radarr_Release_CustomFormatScore", remoteMovie.CustomFormatScore.ToString());
_proxy.SendNotification(variables, Settings); _proxy.SendNotification(variables, Settings);
} }

View File

@@ -1,8 +0,0 @@
namespace NzbDrone.Core.Notifications.Notifiarr
{
public enum NotifiarrEnvironment
{
Live,
Development
}
}

View File

@@ -54,17 +54,19 @@ namespace NzbDrone.Core.Notifications.Notifiarr
_logger.Error(ex, "API key is invalid: " + ex.Message); _logger.Error(ex, "API key is invalid: " + ex.Message);
return new ValidationFailure("APIKey", "API key is invalid"); return new ValidationFailure("APIKey", "API key is invalid");
case 400: case 400:
_logger.Error(ex, "Unable to send test message. Ensure Radarr Integration is enabled & assigned a channel on Notifiarr");
return new ValidationFailure("", "Unable to send test message. Ensure Radarr Integration is enabled & assigned a channel on Notifiarr");
case 520: case 520:
case 521: case 521:
case 522: case 522:
case 523: case 523:
case 524: case 524:
_logger.Error(ex, "Unable to send test notification: " + ex.Message); _logger.Error(ex, "Cloudflare Related HTTP Error - Unable to send test message: " + ex.Message);
return new ValidationFailure("", "Unable to send test notification"); return new ValidationFailure("", "Cloudflare Related HTTP Error - Unable to send test message");
} }
_logger.Error(ex, "Unable to send test message: " + ex.Message); _logger.Error(ex, "Unknown HTTP Error - Unable to send test message: " + ex.Message);
return new ValidationFailure("APIKey", "Unable to send test notification"); return new ValidationFailure("", "Unknown HTTP Error - Unable to send test message");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -77,7 +79,7 @@ namespace NzbDrone.Core.Notifications.Notifiarr
{ {
try try
{ {
var url = settings.Environment == (int)NotifiarrEnvironment.Development ? "https://dev.notifiarr.com" : "https://notifiarr.com"; var url = "https://notifiarr.com";
var requestBuilder = new HttpRequestBuilder(url + "/api/v1/notification/radarr/" + settings.APIKey).Post(); var requestBuilder = new HttpRequestBuilder(url + "/api/v1/notification/radarr/" + settings.APIKey).Post();
requestBuilder.AddFormParameter("instanceName", settings.InstanceName).Build(); requestBuilder.AddFormParameter("instanceName", settings.InstanceName).Build();
@@ -95,19 +97,21 @@ namespace NzbDrone.Core.Notifications.Notifiarr
switch ((int)ex.Response.StatusCode) switch ((int)ex.Response.StatusCode)
{ {
case 401: case 401:
_logger.Error(ex, "API key is invalid"); _logger.Error("", "API key is invalid");
throw; throw new NotifiarrException("API key is invalid", ex);
case 400: case 400:
_logger.Error(ex, "Unable to send notification. Ensure Radarr Integration is enabled & assigned a channel on Notifiarr");
throw new NotifiarrException("Unable to send notification. Ensure Radarr Integration is enabled & assigned a channel on Notifiarr", ex);
case 520: case 520:
case 521: case 521:
case 522: case 522:
case 523: case 523:
case 524: case 524:
_logger.Error(ex, "Unable to send notification"); _logger.Error(ex, "Cloudflare Related HTTP Error - Unable to send notification");
throw; throw new NotifiarrException("Cloudflare Related HTTP Error - Unable to send notification", ex);
} }
throw new NotifiarrException("Unable to send notification", ex); throw new NotifiarrException("Unknown HTTP Error - Unable to send notification", ex);
} }
} }
} }

View File

@@ -21,8 +21,6 @@ namespace NzbDrone.Core.Notifications.Notifiarr
public string APIKey { get; set; } public string APIKey { get; set; }
[FieldDefinition(1, Label = "Instance Name", Advanced = true, HelpText = "Unique name for this instance", HelpLink = "https://notifiarr.com")] [FieldDefinition(1, Label = "Instance Name", Advanced = true, HelpText = "Unique name for this instance", HelpLink = "https://notifiarr.com")]
public string InstanceName { get; set; } public string InstanceName { get; set; }
[FieldDefinition(2, Label = "Environment", Advanced = true, Type = FieldType.Select, SelectOptions = typeof(NotifiarrEnvironment), HelpText = "Live unless told otherwise", HelpLink = "https://notifiarr.com")]
public int Environment { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Movies;
namespace NzbDrone.Core.Notifications.Ntfy
{
public class Ntfy : NotificationBase<NtfySettings>
{
private readonly INtfyProxy _proxy;
public Ntfy(INtfyProxy proxy)
{
_proxy = proxy;
}
public override string Name => "ntfy.sh";
public override string Link => "https://ntfy.sh/";
public override void OnGrab(GrabMessage grabMessage)
{
_proxy.SendNotification(MOVIE_GRABBED_TITLE_BRANDED, grabMessage.Message, Settings);
}
public override void OnDownload(DownloadMessage message)
{
_proxy.SendNotification(MOVIE_DOWNLOADED_TITLE_BRANDED, message.Message, Settings);
}
public override void OnMovieAdded(Movie movie)
{
_proxy.SendNotification(MOVIE_ADDED_TITLE_BRANDED, $"{movie.Title} added to library", Settings);
}
public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage)
{
_proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, Settings);
}
public override void OnMovieDelete(MovieDeleteMessage deleteMessage)
{
_proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message, Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
_proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings);
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_proxy.Test(Settings));
return new ValidationResult(failures);
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Ntfy
{
public class NtfyException : NzbDroneException
{
public NtfyException(string message)
: base(message)
{
}
public NtfyException(string message, Exception innerException, params object[] args)
: base(message, innerException, args)
{
}
}
}

View File

@@ -0,0 +1,11 @@
namespace NzbDrone.Core.Notifications.Ntfy
{
public enum NtfyPriority
{
Min = 1,
Low = 2,
Default = 3,
High = 4,
Max = 5
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Notifications.Ntfy
{
public interface INtfyProxy
{
void SendNotification(string title, string message, NtfySettings settings);
ValidationFailure Test(NtfySettings settings);
}
public class NtfyProxy : INtfyProxy
{
private const string DEFAULT_PUSH_URL = "https://ntfy.sh";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public NtfyProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public void SendNotification(string title, string message, NtfySettings settings)
{
var error = false;
var serverUrl = settings.ServerUrl.IsNullOrWhiteSpace() ? NtfyProxy.DEFAULT_PUSH_URL : settings.ServerUrl;
foreach (var topic in settings.Topics)
{
var request = BuildTopicRequest(serverUrl, topic);
try
{
SendNotification(title, message, request, settings);
}
catch (NtfyException ex)
{
_logger.Error(ex, "Unable to send test message to {0}", topic);
error = true;
}
}
if (error)
{
throw new NtfyException("Unable to send Ntfy notifications to all topics");
}
}
private HttpRequestBuilder BuildTopicRequest(string serverUrl, string topic)
{
var trimServerUrl = serverUrl.TrimEnd('/');
var requestBuilder = new HttpRequestBuilder($"{trimServerUrl}/{topic}").Post();
return requestBuilder;
}
public ValidationFailure Test(NtfySettings settings)
{
try
{
const string title = "Radarr - Test Notification";
const string body = "This is a test message from Radarr";
SendNotification(title, body, settings);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized || ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Error(ex, "Authorization is required");
return new ValidationFailure("UserName", "Authorization is required");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ServerUrl", "Unable to send test message");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private void SendNotification(string title, string message, HttpRequestBuilder requestBuilder, NtfySettings settings)
{
try
{
requestBuilder.Headers.Add("X-Title", title);
requestBuilder.Headers.Add("X-Message", message);
requestBuilder.Headers.Add("X-Priority", settings.Priority.ToString());
if (settings.Tags.Any())
{
requestBuilder.Headers.Add("X-Tags", settings.Tags.Join(","));
}
if (!settings.ClickUrl.IsNullOrWhiteSpace())
{
requestBuilder.Headers.Add("X-Click", settings.ClickUrl);
}
var request = requestBuilder.Build();
if (!settings.UserName.IsNullOrWhiteSpace() && !settings.Password.IsNullOrWhiteSpace())
{
request.Credentials = new BasicNetworkCredential(settings.UserName, settings.Password);
}
_httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized || ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Error(ex, "Authorization is required");
throw;
}
throw new NtfyException("Unable to send text message: {0}", ex, ex.Message);
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Ntfy
{
public class NtfySettingsValidator : AbstractValidator<NtfySettings>
{
public NtfySettingsValidator()
{
RuleFor(c => c.Topics).NotEmpty();
RuleFor(c => c.Priority).InclusiveBetween(1, 5);
RuleFor(c => c.ServerUrl).IsValidUrl().When(c => !c.ServerUrl.IsNullOrWhiteSpace());
RuleFor(c => c.ClickUrl).IsValidUrl().When(c => !c.ClickUrl.IsNullOrWhiteSpace());
RuleFor(c => c.UserName).NotEmpty().When(c => !c.Password.IsNullOrWhiteSpace());
RuleFor(c => c.Password).NotEmpty().When(c => !c.UserName.IsNullOrWhiteSpace());
RuleForEach(c => c.Topics).NotEmpty().Matches("[a-zA-Z0-9_-]+").Must(c => !InvalidTopics.Contains(c)).WithMessage("Invalid topic");
}
private static List<string> InvalidTopics => new List<string> { "announcements", "app", "docs", "settings", "stats", "mytopic-rw", "mytopic-ro", "mytopic-wo" };
}
public class NtfySettings : IProviderConfig
{
private static readonly NtfySettingsValidator Validator = new NtfySettingsValidator();
public NtfySettings()
{
Topics = Array.Empty<string>();
Priority = 3;
}
[FieldDefinition(0, Label = "Server Url", Type = FieldType.Url, HelpLink = "https://ntfy.sh/docs/install/", HelpText = "Leave blank to use public server (https://ntfy.sh)", Placeholder = "https://ntfy.sh")]
public string ServerUrl { get; set; }
[FieldDefinition(1, Label = "User Name", HelpText = "Optional Authorization", Privacy = PrivacyLevel.UserName)]
public string UserName { get; set; }
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Optional Password", Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(3, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NtfyPriority))]
public int Priority { get; set; }
[FieldDefinition(4, Label = "Topics", HelpText = "List of Topics to send notifications to", Type = FieldType.Tag, Placeholder = "Topic1234,Topic4321")]
public IEnumerable<string> Topics { get; set; }
[FieldDefinition(5, Label = "Ntfy Tags and Emojis", Type = FieldType.Tag, HelpText = "Optional list of tags or emojis to use", Placeholder = "warning,skull", HelpLink = "https://ntfy.sh/docs/emojis/")]
public IEnumerable<string> Tags { get; set; }
[FieldDefinition(6, Label = "Click Url", Type = FieldType.Url, HelpText = "Optional link when user clicks notification", Placeholder = "https://myserver.example.com/radarr")]
public string ClickUrl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -38,20 +38,9 @@ namespace NzbDrone.Core.Organizer
private readonly ICustomFormatService _formatService; private readonly ICustomFormatService _formatService;
private readonly Logger _logger; private readonly Logger _logger;
private static readonly Regex TitleRegex = new Regex(@"\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9|]+))?(?<suffix>[- ._)\]]*)\}", private static readonly Regex TitleRegex = new Regex(@"(?<tag>\{(?:imdb-|edition-))?\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9|]+))?(?<suffix>[-} ._)\]]*)\}",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private static readonly Regex TagsRegex = new Regex(@"(?<tags>\{tags(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?<separator>(?<=})[- ._]+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>[- ._]?[ex])(?<episode>{episode(?:\:0+)?}))(?<separator>[- ._]+?(?={))?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?<separator>(?<=})[- ._]+?)?(?<absolute>{absolute(?:\:0+)?})(?<separator>[- ._]+?(?={))?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex MovieTitleRegex = new Regex(@"(?<token>\{((?:(Movie|Original))(?<separator>[- ._])(Clean|Original)?(Title|Filename)(The)?)(?::(?<customFormat>[a-z0-9|]+))?\})", public static readonly Regex MovieTitleRegex = new Regex(@"(?<token>\{((?:(Movie|Original))(?<separator>[- ._])(Clean|Original)?(Title|Filename)(The)?)(?::(?<customFormat>[a-z0-9|]+))?\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -61,11 +50,6 @@ namespace NzbDrone.Core.Organizer
private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
//TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc)
private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' };
private static readonly Regex TitlePrefixRegex = new Regex(@"^(The|An|A) (.*?)((?: *\([^)]+\))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex TitlePrefixRegex = new Regex(@"^(The|An|A) (.*?)((?: *\([^)]+\))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ReservedDeviceNamesRegex = new Regex(@"^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)\.", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ReservedDeviceNamesRegex = new Regex(@"^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)\.", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -133,7 +117,7 @@ namespace NzbDrone.Core.Organizer
AddQualityTokens(tokenHandlers, movie, movieFile); AddQualityTokens(tokenHandlers, movie, movieFile);
AddMediaInfoTokens(tokenHandlers, movieFile); AddMediaInfoTokens(tokenHandlers, movieFile);
AddMovieFileTokens(tokenHandlers, movieFile); AddMovieFileTokens(tokenHandlers, movieFile);
AddTagsTokens(tokenHandlers, movieFile); AddEditionTagsTokens(tokenHandlers, movieFile);
AddCustomFormats(tokenHandlers, movie, movieFile, customFormats); AddCustomFormats(tokenHandlers, movie, movieFile, customFormats);
var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
@@ -193,7 +177,7 @@ namespace NzbDrone.Core.Organizer
AddQualityTokens(tokenHandlers, movie, movieFile); AddQualityTokens(tokenHandlers, movie, movieFile);
AddMediaInfoTokens(tokenHandlers, movieFile); AddMediaInfoTokens(tokenHandlers, movieFile);
AddMovieFileTokens(tokenHandlers, movieFile); AddMovieFileTokens(tokenHandlers, movieFile);
AddTagsTokens(tokenHandlers, movieFile); AddEditionTagsTokens(tokenHandlers, movieFile);
} }
else else
{ {
@@ -297,7 +281,7 @@ namespace NzbDrone.Core.Organizer
return movie.Title; return movie.Title;
} }
private void AddTagsTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile) private void AddEditionTagsTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile)
{ {
if (movieFile.Edition.IsNotNullOrWhiteSpace()) if (movieFile.Edition.IsNotNullOrWhiteSpace())
{ {
@@ -497,6 +481,7 @@ namespace NzbDrone.Core.Organizer
var tokenMatch = new TokenMatch var tokenMatch = new TokenMatch
{ {
RegexMatch = match, RegexMatch = match,
Tag = match.Groups["tag"].Value,
Prefix = match.Groups["prefix"].Value, Prefix = match.Groups["prefix"].Value,
Separator = match.Groups["separator"].Value, Separator = match.Groups["separator"].Value,
Suffix = match.Groups["suffix"].Value, Suffix = match.Groups["suffix"].Value,
@@ -531,7 +516,7 @@ namespace NzbDrone.Core.Organizer
if (!replacementText.IsNullOrWhiteSpace()) if (!replacementText.IsNullOrWhiteSpace())
{ {
replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; replacementText = tokenMatch.Tag + tokenMatch.Prefix + replacementText + tokenMatch.Suffix;
} }
return replacementText; return replacementText;
@@ -598,6 +583,7 @@ namespace NzbDrone.Core.Organizer
internal sealed class TokenMatch internal sealed class TokenMatch
{ {
public Match RegexMatch { get; set; } public Match RegexMatch { get; set; }
public string Tag { get; set; }
public string Prefix { get; set; } public string Prefix { get; set; }
public string Separator { get; set; } public string Separator { get; set; }
public string Suffix { get; set; } public string Suffix { get; set; }

View File

@@ -42,7 +42,8 @@ namespace NzbDrone.Core.Parser
new IsoLanguage("uk", "", "ukr", "Ukrainian", Language.Ukrainian), new IsoLanguage("uk", "", "ukr", "Ukrainian", Language.Ukrainian),
new IsoLanguage("fa", "", "fas", "Persian", Language.Persian), new IsoLanguage("fa", "", "fas", "Persian", Language.Persian),
new IsoLanguage("be", "", "ben", "Bengali", Language.Bengali), new IsoLanguage("be", "", "ben", "Bengali", Language.Bengali),
new IsoLanguage("lt", "", "lit", "Lithuanian", Language.Lithuanian) new IsoLanguage("lt", "", "lit", "Lithuanian", Language.Lithuanian),
new IsoLanguage("sk", "", "slk", "Slovak", Language.Slovak)
}; };
public static IsoLanguage Find(string isoCode) public static IsoLanguage Find(string isoCode)

View File

@@ -32,7 +32,8 @@ namespace NzbDrone.Core.Parser
private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)| private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|
(?<czech>\bCZ\b)| (?<czech>\bCZ\b)|
(?<polish>\bPL\b)| (?<polish>\bPL\b)|
(?<bulgarian>\bBG\b))(?:(?i)(?![\W|_|^]SUB))", (?<bulgarian>\bBG\b))(?:(?i)(?![\W|_|^]SUB))|
(?<slovak>\bSK\b)",
RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})(?:[-_. ]forced)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})(?:[-_. ]forced)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -182,6 +183,11 @@ namespace NzbDrone.Core.Parser
languages.Add(Language.Bengali); languages.Add(Language.Bengali);
} }
if (lowerTitle.Contains("slovak"))
{
languages.Add(Language.Slovak);
}
// Case sensitive // Case sensitive
var caseSensitiveMatch = CaseSensitiveLanguageRegex.Match(title); var caseSensitiveMatch = CaseSensitiveLanguageRegex.Match(title);
@@ -205,6 +211,11 @@ namespace NzbDrone.Core.Parser
languages.Add(Language.Bulgarian); languages.Add(Language.Bulgarian);
} }
if (caseSensitiveMatch.Groups["slovak"].Captures.Cast<Capture>().Any())
{
languages.Add(Language.Slovak);
}
var matches = LanguageRegex.Matches(title); var matches = LanguageRegex.Matches(title);
foreach (Match match in matches) foreach (Match match in matches)

View File

@@ -147,11 +147,11 @@ namespace NzbDrone.Core.Parser
//Handle Exception Release Groups that don't follow -RlsGrp; Manual List //Handle Exception Release Groups that don't follow -RlsGrp; Manual List
//groups whose releases end with RlsGroup) or RlsGroup] //groups whose releases end with RlsGroup) or RlsGroup]
private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<releasegroup>(Tigole|Joy|YIFY|YTS.MX|YTS.LT|FreetheFish|afm72|Anna|Bandi|Ghost|Kappa|MONOLITH|Qman|RZeroX|SAMPA|Silence|theincognito|t3nzin|Vyndros|HDO|DusIctv|DHD|SEV|CtrlHD|-ZR-|ADC|XZVN|RH|Kametsu|r00t)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<releasegroup>(Joy|YIFY|YTS.MX|YTS.LT|FreetheFish|afm72|Anna|Bandi|Ghost|Kappa|MONOLITH|Qman|RZeroX|SAMPA|Silence|theincognito|t3nzin|Vyndros|HDO|DusIctv|DHD|SEV|CtrlHD|-ZR-|ADC|XZVN|RH|Kametsu|r00t|HONE)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled);
//Handle Exception Release Groups that don't follow -RlsGrp; Manual List //Handle Exception Release Groups that don't follow -RlsGrp; Manual List
// name only...BE VERY CAREFUL WITH THIS, HIGH CHANCE OF FALSE POSITIVES // name only...BE VERY CAREFUL WITH THIS, HIGH CHANCE OF FALSE POSITIVES
private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>KRaLiMaRKo|E\.N\.D|D\-Z0N3|Koten_Gars|BluDragon)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>KRaLiMaRKo|E\.N\.D|D\-Z0N3|Koten_Gars|BluDragon|ZØNEHD|Tigole)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|'|\|)+", RegexOptions.Compiled); private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|'|\|)+", RegexOptions.Compiled);
private static readonly Regex SpecialCharRegex = new Regex(@"(\&|\:|\\|\/)+", RegexOptions.Compiled); private static readonly Regex SpecialCharRegex = new Regex(@"(\&|\:|\\|\/)+", RegexOptions.Compiled);
@@ -401,7 +401,7 @@ namespace NzbDrone.Core.Parser
return null; return null;
} }
public static string ToUrlSlug(string value) public static string ToUrlSlug(string value, bool invalidDashReplacement = false, string trimEndChars = "-_", string deduplicateChars = "-_")
{ {
//First to lower case //First to lower case
value = value.ToLowerInvariant(); value = value.ToLowerInvariant();
@@ -412,14 +412,23 @@ namespace NzbDrone.Core.Parser
//Replace spaces //Replace spaces
value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled); value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled);
//Should invalid characters be replaced with dash or empty string?
string replaceCharacter = invalidDashReplacement ? "-" : string.Empty;
//Remove invalid chars //Remove invalid chars
value = Regex.Replace(value, @"[^a-z0-9\s-_]", "", RegexOptions.Compiled); value = Regex.Replace(value, @"[^a-z0-9\s-_]", replaceCharacter, RegexOptions.Compiled);
//Trim dashes from end //Trim dashes or underscores from end, or user defined character set
value = value.Trim('-', '_'); if (!string.IsNullOrEmpty(trimEndChars))
{
value = value.Trim(trimEndChars.ToCharArray());
}
//Replace double occurences of - or _ //Replace double occurrences of - or _, or user defined character set
value = Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled); if (!string.IsNullOrEmpty(deduplicateChars))
{
value = Regex.Replace(value, @"([" + deduplicateChars + "]){2,}", "$1", RegexOptions.Compiled);
}
return value; return value;
} }

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Parser
(?<scr>SCR|SCREENER|DVDSCR|DVDSCREENER)| (?<scr>SCR|SCREENER|DVDSCR|DVDSCREENER)|
(?<ts>TS[-_. ]|TELESYNC|HD-TS|HDTS|PDVD|TSRip|HDTSRip)| (?<ts>TS[-_. ]|TELESYNC|HD-TS|HDTS|PDVD|TSRip|HDTSRip)|
(?<tc>TC|TELECINE|HD-TC|HDTC)| (?<tc>TC|TELECINE|HD-TC|HDTC)|
(?<cam>CAMRIP|CAM|HDCAM|HD-CAM)| (?<cam>CAMRIP|CAM|HDCAM|HQCAM|HD-CAM)|
(?<wp>WORKPRINT|WP)| (?<wp>WORKPRINT|WP)|
(?<pdtv>PDTV)| (?<pdtv>PDTV)|
(?<sdtv>SDTV)| (?<sdtv>SDTV)|

View File

@@ -5,24 +5,24 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Equ" Version="2.3.0" /> <PackageReference Include="Equ" Version="2.3.0" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
<PackageReference Include="MailKit" Version="2.15.0" /> <PackageReference Include="MailKit" Version="2.15.0" />
<PackageReference Include="Npgsql" Version="6.0.3" /> <PackageReference Include="Npgsql" Version="6.0.3" />
<PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" /> <PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" />
<PackageReference Include="Servarr.FFprobe" Version="5.0.1.93" /> <PackageReference Include="Servarr.FFprobe" Version="5.0.1.93" />
<PackageReference Include="System.Memory" Version="4.5.4" /> <PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" /> <PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" /> <PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageReference Include="FluentValidation" Version="8.6.2" /> <PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="MonoTorrent" Version="2.0.5" /> <PackageReference Include="MonoTorrent" Version="2.0.5" />
<PackageReference Include="System.Text.Json" Version="6.0.4" /> <PackageReference Include="System.Text.Json" Version="6.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Radarr.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Common\Radarr.Common.csproj" />

View File

@@ -148,12 +148,18 @@ namespace NzbDrone.Core.RootFolders
var possibleMovieFolders = _diskProvider.GetDirectories(path).ToList(); var possibleMovieFolders = _diskProvider.GetDirectories(path).ToList();
var unmappedFolders = possibleMovieFolders.Except(moviePaths.Select(s => s.Value), PathEqualityComparer.Instance).ToList(); var unmappedFolders = possibleMovieFolders.Except(moviePaths.Select(s => s.Value), PathEqualityComparer.Instance).ToList();
foreach (string unmappedFolder in unmappedFolders) var recycleBinPath = _configService.RecycleBin;
foreach (var unmappedFolder in unmappedFolders)
{ {
var di = new DirectoryInfo(unmappedFolder.Normalize()); var di = new DirectoryInfo(unmappedFolder.Normalize());
if ((!di.Attributes.HasFlag(FileAttributes.System) && !di.Attributes.HasFlag(FileAttributes.Hidden)) || di.Attributes.ToString() == "-1") if ((!di.Attributes.HasFlag(FileAttributes.System) && !di.Attributes.HasFlag(FileAttributes.Hidden)) || di.Attributes.ToString() == "-1")
{ {
results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName }); if (string.IsNullOrWhiteSpace(recycleBinPath) || di.FullName.PathNotEquals(recycleBinPath))
{
results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName });
}
} }
} }

View File

@@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />

View File

@@ -60,7 +60,7 @@ namespace NzbDrone.Test.Common
} }
else else
{ {
Start(Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "bin", consoleExe)); Start(Path.Combine(TestContext.CurrentContext.TestDirectory, "bin", consoleExe));
} }
while (true) while (true)

View File

@@ -53,11 +53,25 @@ namespace Radarr.Api.V3.Collections
} }
[HttpGet] [HttpGet]
public List<CollectionResource> GetCollections() public List<CollectionResource> GetCollections(int? tmdbId)
{ {
var collectionMovies = _movieMetadataService.GetMoviesWithCollections(); var collectionResources = new List<CollectionResource>();
return MapToResource(_collectionService.GetAllCollections(), collectionMovies).ToList(); if (tmdbId.HasValue)
{
var collection = _collectionService.FindByTmdbId(tmdbId.Value);
if (collection != null)
{
collectionResources.AddIfNotNull(MapToResource(collection));
}
}
else
{
collectionResources = MapToResource(_collectionService.GetAllCollections()).ToList();
}
return collectionResources;
} }
[RestPutById] [RestPutById]
@@ -116,10 +130,11 @@ namespace Radarr.Api.V3.Collections
return Accepted(updated); return Accepted(updated);
} }
private IEnumerable<CollectionResource> MapToResource(List<MovieCollection> collections, List<MovieMetadata> collectionMovies) private IEnumerable<CollectionResource> MapToResource(List<MovieCollection> collections)
{ {
// Avoid calling for naming spec on every movie in filenamebuilder // Avoid calling for naming spec on every movie in filenamebuilder
var namingConfig = _namingService.GetConfig(); var namingConfig = _namingService.GetConfig();
var collectionMovies = _movieMetadataService.GetMoviesWithCollections();
foreach (var collection in collections) foreach (var collection in collections)
{ {
@@ -167,7 +182,7 @@ namespace Radarr.Api.V3.Collections
[NonAction] [NonAction]
public void Handle(CollectionDeletedEvent message) public void Handle(CollectionDeletedEvent message)
{ {
BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.Collection)); BroadcastResourceChange(ModelAction.Deleted, message.Collection.Id);
} }
} }
} }

View File

@@ -2,10 +2,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation; using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.CustomFormats; using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Validation;
using Radarr.Http; using Radarr.Http;
using Radarr.Http.REST; using Radarr.Http.REST;
using Radarr.Http.REST.Attributes; using Radarr.Http.REST.Attributes;
@@ -50,6 +52,9 @@ namespace Radarr.Api.V3.CustomFormats
public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource) public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource)
{ {
var model = customFormatResource.ToModel(_specifications); var model = customFormatResource.ToModel(_specifications);
Validate(model);
return Created(_formatService.Insert(model).Id); return Created(_formatService.Insert(model).Id);
} }
@@ -57,6 +62,9 @@ namespace Radarr.Api.V3.CustomFormats
public ActionResult<CustomFormatResource> Update(CustomFormatResource resource) public ActionResult<CustomFormatResource> Update(CustomFormatResource resource)
{ {
var model = resource.ToModel(_specifications); var model = resource.ToModel(_specifications);
Validate(model);
_formatService.Update(model); _formatService.Update(model);
return Accepted(model.Id); return Accepted(model.Id);
@@ -89,6 +97,25 @@ namespace Radarr.Api.V3.CustomFormats
return schema; return schema;
} }
private void Validate(CustomFormat definition)
{
foreach (var spec in definition.Specifications)
{
var validationResult = spec.Validate();
VerifyValidationResult(validationResult);
}
}
protected void VerifyValidationResult(ValidationResult validationResult)
{
var result = new NzbDroneValidationResult(validationResult.Errors);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
private IEnumerable<ICustomFormatSpecification> GetPresets() private IEnumerable<ICustomFormatSpecification> GetPresets()
{ {
yield return new ReleaseTitleSpecification yield return new ReleaseTitleSpecification

View File

@@ -11,6 +11,7 @@ using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using Radarr.Api.V3.Movies;
using Radarr.Http; using Radarr.Http;
namespace Radarr.Api.V3.ImportLists namespace Radarr.Api.V3.ImportLists
@@ -19,6 +20,7 @@ namespace Radarr.Api.V3.ImportLists
public class ImportListMoviesController : Controller public class ImportListMoviesController : Controller
{ {
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IAddMovieService _addMovieService;
private readonly IProvideMovieInfo _movieInfo; private readonly IProvideMovieInfo _movieInfo;
private readonly IBuildFileNames _fileNameBuilder; private readonly IBuildFileNames _fileNameBuilder;
private readonly IImportListMovieService _listMovieService; private readonly IImportListMovieService _listMovieService;
@@ -28,6 +30,7 @@ namespace Radarr.Api.V3.ImportLists
private readonly IConfigService _configService; private readonly IConfigService _configService;
public ImportListMoviesController(IMovieService movieService, public ImportListMoviesController(IMovieService movieService,
IAddMovieService addMovieService,
IProvideMovieInfo movieInfo, IProvideMovieInfo movieInfo,
IBuildFileNames fileNameBuilder, IBuildFileNames fileNameBuilder,
IImportListMovieService listMovieService, IImportListMovieService listMovieService,
@@ -37,6 +40,7 @@ namespace Radarr.Api.V3.ImportLists
IConfigService configService) IConfigService configService)
{ {
_movieService = movieService; _movieService = movieService;
_addMovieService = addMovieService;
_movieInfo = movieInfo; _movieInfo = movieInfo;
_fileNameBuilder = fileNameBuilder; _fileNameBuilder = fileNameBuilder;
_listMovieService = listMovieService; _listMovieService = listMovieService;
@@ -92,6 +96,14 @@ namespace Radarr.Api.V3.ImportLists
return realResults; return realResults;
} }
[HttpPost]
public object AddMovies([FromBody] List<MovieResource> resource)
{
var newMovies = resource.ToModel();
return _addMovieService.AddMovies(newMovies, true).ToResource(0);
}
private IEnumerable<ImportListMoviesResource> MapToResource(IEnumerable<Movie> movies, Language language) private IEnumerable<ImportListMoviesResource> MapToResource(IEnumerable<Movie> movies, Language language)
{ {
//Avoid calling for naming spec on every movie in filenamebuilder //Avoid calling for naming spec on every movie in filenamebuilder

View File

@@ -94,6 +94,8 @@ namespace Radarr.Api.V3.Movies
var translatedTitle = movieTranslation?.Title ?? model.Title; var translatedTitle = movieTranslation?.Title ?? model.Title;
var translatedOverview = movieTranslation?.Overview ?? model.MovieMetadata.Value.Overview; var translatedOverview = movieTranslation?.Overview ?? model.MovieMetadata.Value.Overview;
var collection = model.MovieMetadata.Value.CollectionTmdbId > 0 ? new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId } : null;
return new MovieResource return new MovieResource
{ {
Id = model.Id, Id = model.Id,
@@ -141,7 +143,7 @@ namespace Radarr.Api.V3.Movies
MovieFile = movieFile, MovieFile = movieFile,
YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId, YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId,
Studio = model.MovieMetadata.Value.Studio, Studio = model.MovieMetadata.Value.Studio,
Collection = new MovieCollection { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId }, Collection = collection,
Popularity = model.MovieMetadata.Value.Popularity Popularity = model.MovieMetadata.Value.Popularity
}; };
} }

View File

@@ -589,6 +589,16 @@
"tags": [ "tags": [
"Collection" "Collection"
], ],
"parameters": [
{
"name": "tmdbId",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "Success", "description": "Success",
@@ -3315,6 +3325,44 @@
"description": "Success" "description": "Success"
} }
} }
},
"post": {
"tags": [
"ImportListMovies"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MovieResource"
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MovieResource"
}
}
},
"application/*+json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MovieResource"
}
}
}
}
},
"responses": {
"200": {
"description": "Success"
}
}
} }
}, },
"/api/v3/indexer": { "/api/v3/indexer": {

2096
yarn.lock

File diff suppressed because it is too large Load Diff