1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-30 18:25:57 -04:00

Compare commits

...

84 Commits

Author SHA1 Message Date
Stevie Robinson
5a64826868 Add: New icon for deleted episodes with status missing from disk
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>
(cherry picked from commit 79907c881cc92ce9ee3973d5cf21749fe5fc58da)

Closes #9604
2024-01-14 03:47:30 +02:00
Mark McDowall
cda40312e0 New: Optional directory setting for Aria2
(cherry picked from commit fd17df0dd03a5feb088c3241a247eac20f0e8c6c)

Closes #9602
2024-01-14 03:43:37 +02:00
Bogdan
907779b4ce Fetch movie file entity from database to broadcast 2024-01-14 03:42:03 +02:00
Mark McDowall
cc03651af5 Don't use TestCase for single test
(cherry picked from commit 541d3307e1466b0353dc4149f502a4b62b4de616)
2024-01-14 03:41:26 +02:00
servarr[bot]
1ae98d618c Fixed: Movie posters flickering when width changes repeatedly
(cherry picked from commit 53cf5308931069638c23925596a3fd8aaccc5d98)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-01-14 03:40:54 +02:00
Bogdan
f5914da2f9 Remove double filtering in entity history repository 2024-01-12 22:05:51 +02:00
Bogdan
f7816aa5cd Fixed: Filter history by multiple event types in PG 2024-01-12 22:05:51 +02:00
Andrejs Ķīlis
a652ce50a9 Fixed: Latvian and Russian language parsing
Improved support for Latvian with test cases I have encountered in the wild and fixed a case where Russian is not recognized (RU instead of RUS).
2024-01-12 03:00:51 +02:00
Bogdan
58b726a292 Fixed: Improve torrent blocklist matching
Closes #9585
2024-01-12 02:56:32 +02:00
Bogdan
1d8cf6a7f5 Fixed: Persist release source for pending releases
Closes #9583
2024-01-12 02:54:31 +02:00
ilike2burnthing
2c3ad380ef Remove unsupported pagination for Nyaa
(cherry picked from commit fef525ddb8b5f91bb36b3c9e652663fccb098a00)

Closes #9582
2024-01-12 02:52:53 +02:00
Stevie Robinson
0e7874aacf Fix Missing HelpText Translation Keys
(cherry picked from commit 587b600d6c6bac64c99d12225360810ef283f0aa)

Closes #9576
2024-01-12 02:45:57 +02:00
Weblate
8638d82ad3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aleksandr <alyarmak@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Bradley BARBIER <bradley.barbier@outlook.fr>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: DimitriDR <dimitridroeck@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: HuaBing <admin@hbcraft.cn>
Co-authored-by: JJonttuu <oikeaihminen@protonmail.com>
Co-authored-by: Juan Lores <juan.lores@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Piotr Komborski <piotr+github@kombor.ski>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Watashi <drazy24@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: boan51204 <je.991707@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: liangyi <994302767@qq.com>
Co-authored-by: marius-tu <marius.tubbesing94@gmail.com>
Co-authored-by: ragote <ragote@pm.me>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: twobuttonbob <madinlol@gmail.com>
Co-authored-by: 饶志华 <879467666@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
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/lv/
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/uk/
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
2024-01-12 02:41:40 +02:00
Bogdan
f3d6a1f99d Fixed: Release source for release/push 2024-01-11 02:07:03 +02:00
Bogdan
fa036f5807 Sorting movie list by tags 2024-01-11 00:41:53 +02:00
Bogdan
a931f8a69f Fixed: Skip fewer slides with cast/crew on smaller screens
Fixes #9571
2024-01-10 20:51:40 +02:00
Bogdan
a491c9a4a0 Fixed: Parsing custom formats for releases titles containing colon 2024-01-10 20:34:24 +02:00
Bogdan
2aafb6369c Fix app name in healthcheck 2024-01-10 01:35:08 +02:00
Mark McDowall
ef8253044e Fixed: Blocklisting torrents from indexers that do not provide torrent hash
(cherry picked from commit 3541cd7ba877fb785c7f97123745abf51162eb8e)
2024-01-09 00:30:32 +02:00
Bogdan
c1feeb72ee New: Year specification for custom formats 2024-01-08 02:29:29 +02:00
Servarr
21560cd6cc Automated API Docs update 2024-01-07 16:36:16 +02:00
Bogdan
bda2b9b0b8 Fixed: Filter history by multiple event types 2024-01-07 16:07:36 +02:00
Bogdan
4630de9616 Bump version to 5.3.1 2024-01-07 11:10:52 +02:00
Bogdan
7e83180e50 Remove title for actions in movie history
Closes #9549
2024-01-04 13:24:28 +02:00
Stevie Robinson
e60eed49c7 Translate Notifications settings
(cherry picked from commit 8f7f23c9380036e87669fa663e846321cf7ebf87)

Closes #9550
2024-01-04 12:49:26 +02:00
Gabriel Patzleiner
74cfc94b4c New: Correctly parse German DL and ML tags in releases 2024-01-02 18:58:28 -06:00
Gabriel Patzleiner
213c55c7af Fixed: Don't parse some movies with German in the movie title
fixes #6474
2024-01-02 18:58:28 -06:00
Gabriel Patzleiner
c066fa5e27 Delete tests that are not needed and not working anymore since 7ec0fd1cea 2024-01-02 18:58:28 -06:00
Gabriel Patzleiner
2741ecb968 Added new IndexerBaseFixture to test Multi tag in releases 2024-01-02 18:58:28 -06:00
Qstick
7965c29425 Fixed: Change "Manual Import" to "Manage Files" in MovieDetails
Prevent confusion with interactive search icon being identical and align to Sonarr naming.
2024-01-01 11:35:11 -06:00
Bogdan
d2cbab70a9 New: Confirmation for searching movies 2024-01-01 17:35:01 +02:00
Weblate
16381a1aef Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mario Rodriguez <mario2423@gmail.com>
Co-authored-by: Norbi <kovinor123@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2024-01-01 14:11:00 +02:00
Bogdan
b92e08b850 Fixed: Disable movie search button if none are listed 2024-01-01 08:20:35 +02:00
Bogdan
eab470c67f New: Movie search will look for movies that haven't been searched recently first 2024-01-01 08:20:35 +02:00
Mark McDowall
7f11659d95 New: Store last search time for MovieSearch
(cherry picked from commit 9af57c6786)
2024-01-01 08:20:35 +02:00
Mark McDowall
03dec07cbe Fixed: Disable SSL on start if certificate path is not set
(cherry picked from commit 4e19fec123900b8ba1252b640f26f2a4983683ff)
2023-12-31 18:40:21 -06:00
Qstick
554c696ee6 Fixed: MovieDetails size incorrect when moviefile store changes
Use movie prop instead

Closes #9309
2023-12-31 16:47:44 -06:00
Qstick
093f8a39fe New: Custom sort crew by job in movie details 2023-12-31 12:55:09 -06:00
Servarr
8a1663f136 Automated API Docs update 2023-12-31 12:03:51 -06:00
bakerboy448
251d2dde97 Improve Import Custom Format Compare Logging 2023-12-31 11:37:02 -06:00
Qstick
996542a4a5 Reduce size of Collection on Movie endpoint
Ensures we don't send false data and reduces the object size to only what's necessary here.

Closes #9521
2023-12-31 11:29:32 -06:00
randomllama
0914d6250c New: Add Movie Status to Kodi .nfo
Closes #9115

(cherry picked from commit b76de3987b0c30e6509d37d82e3163d067a9c6c8)
2023-12-31 11:19:25 -06:00
Bogdan
3ff8e511b5 New: Tags field for Discord 2023-12-31 18:58:39 +02:00
Qstick
3a7b27fb45 Fixed: Parse HebDubbed as Hebrew
Fixes #9513
2023-12-31 10:48:13 -06:00
Weblate
c81d2c97f5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Koch Norbert <kochnorbert@icloud.com>
Co-authored-by: Nicola <nicola.neri@gmail.com>
Co-authored-by: SunStorm <me@sunstorm.rocks>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: chiral-lab <jan.eltner@googlemail.com>
Co-authored-by: chrizl <chrizl@gmail.com>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Co-authored-by: slammingdeath <sebastianbrudny97@gmail.com>
Co-authored-by: ube <ube@alienautopsy.net>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
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/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translation: Servarr/Radarr
2023-12-31 12:26:08 +02:00
Bogdan
dae46524c4 Fix possible multiple enumeration in update collections 2023-12-31 09:49:17 +02:00
Stevie Robinson
3c6386f318 Translate fields on the backend
(cherry picked from commit 48b12f5b00429a7cd218d23f0544641b0da62a06)
2023-12-31 09:38:03 +02:00
Mark McDowall
1400a8806d New: Add qBittorrent option for Content Layout
(cherry picked from commit 4b22200708ca120cfdcf9cb796be92183adb95d1)

Closes #9522
2023-12-31 09:38:03 +02:00
Stevie Robinson
e3f33f5a61 New: Add sorting to Manage Indexer and Download Client modals
(cherry picked from commit 91053ca51ded804739f94ee936c1376a755dbe11)

Closes #9524
2023-12-31 09:38:03 +02:00
Stevie Robinson
e6f4b88cf3 New: Show Proper or Repack tag in interactive search
(cherry picked from commit efb000529b5dff42829df3ef151e4750a7b15cf6)

Closes #9523
2023-12-31 09:38:03 +02:00
Bogdan
b788464487 Fixed: Show errors when adding Root Folder
(cherry picked from commit 16d60a6586aeb458601214258da021ee154e5b6e)

Closes #9527
2023-12-31 09:38:03 +02:00
Bogdan
e29717ec6c New: Retry on failed downloads of torrent and nzb files
(cherry picked from commit bc20ef73bdd47b7cdad43d4c7d4b4bd534e49252)

Closes #9528
2023-12-31 09:38:03 +02:00
Bogdan
5d7e23092f Bump version to 5.3.0 2023-12-31 09:38:03 +02:00
Bogdan
9921d51451 Cleanup unused code in movie credit posters 2023-12-25 15:45:33 +02:00
Bogdan
213620cb29 Fixed: Navigation for cast and crew 2023-12-25 14:56:41 +02:00
Bogdan
bdc4aade0f Use extra release fields in PassThePopcorn parser 2023-12-24 06:56:48 +02:00
Weblate
b2300dbf41 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Pietro Ribeiro <xxb1exuv6@mozmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translation: Servarr/Radarr
2023-12-23 23:31:39 +02:00
Weblate
44289d30f9 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Aitzol Garmendia <aitzolgarmendia@gmail.com>
Co-authored-by: Andrés Reyes Monge <armonge@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Michael Schönenberger <muchi94@gmail.com>
Co-authored-by: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-22 04:24:19 +02:00
luz paz
260fb88f85 Fix various typos
Found via `codespell -q 3`

(cherry picked from commit 209a250079fdf7ad2bc9168f81bfb45b9531d6b3)
2023-12-19 20:18:54 +02:00
Bogdan
119cdf6f09 Fixed: Cleanup orphaned import list movies by movie metadata 2023-12-18 00:51:59 +02:00
Bogdan
c8d30fd214 Cleanup convert root folders to TS 2023-12-17 23:23:08 +02:00
Bogdan
7e9e528d3b Fixed: Ignore empty tags when adding items to Flood
Fixed #8145
2023-12-17 22:09:13 +02:00
Bogdan
8554c0d9cb Refactor movie alternative titles connector 2023-12-17 19:57:22 +02:00
Bogdan
22cc34b4fe Bump version to 5.2.6 2023-12-17 16:01:10 +02:00
Weblate
990785ebfc Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Menno Liefstingh <mennoliefstingh@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: lifeisfreedom048 <koyuncu.ozgur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2023-12-16 02:41:57 +02:00
Bogdan
957be99401 Fixed: Bump media info revision for DV HDR10Plus 2023-12-16 02:41:34 +02:00
Bogdan
4bcde25e29 Improve messaging for accepted Custom Formats scoring upgrades
Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>

Closes #9496
2023-12-16 00:38:05 +02:00
Bogdan
1d70f36e7d New: 3D and HDR metadata for Trakt connection 2023-12-15 17:13:47 +02:00
Bogdan
cc0a448bc8 New: Rate limiting for Trakt connection 2023-12-15 17:13:47 +02:00
Bogdan
c9e977baea Simplify mapping in Trakt connection 2023-12-15 17:13:47 +02:00
Mark McDowall
6cb9a46cd4 Fixed: Imported movies updating on Calendar
(cherry picked from commit 5a3bc49392b700650a34536ff3794bce614f64a4)

Closes #9491
2023-12-15 16:50:06 +02:00
Agneev Mukherjee
eef379277a Enable browser navigation buttons for PWA
(cherry picked from commit da9a60691f363323565a293ed9eaeb6349ceccb6)

Closes #9487
2023-12-15 16:36:28 +02:00
Chad A Simmons
41fef47684 New: Support for DV HDR10Plus from media info 2023-12-15 03:36:32 +02:00
Weblate
fcda6faf3d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ROSERAT Ugo <roserat.ugo@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-13 16:41:49 +02:00
Bogdan
79bbf9c50b Fixed: Movie status label in add movie search results 2023-12-12 21:55:41 +02:00
Bogdan
43d2f2804b New: IMDb ratings and genres in add movie search results 2023-12-12 21:55:09 +02:00
Qstick
fa62f3f66a Fixed: Correctly handle Migration when PG Host has ".db"
(cherry picked from commit 97ee24507f4306e3b62c3d00cd3ade6a09d1b957)

Closes #9478
2023-12-12 15:36:21 +02:00
Bogdan
229d91fe40 Implement DatabaseConnectionInfo
Co-authored-by: Qstick <qstick@gmail.com>
2023-12-12 15:36:14 +02:00
Bogdan
2673d1eee4 Fixed: Movie poster in search results after adding
Fixes #8029
2023-12-11 19:30:31 +02:00
Bogdan
e59fd1118f Fixed: Downloading status post-adding movie 2023-12-11 19:27:05 +02:00
Bogdan
c1fd33b152 Fix categories for NZBFinder 2023-12-10 15:50:19 +02:00
Bogdan
2f58c8676f Bump dotnet to 6.0.25 2023-12-10 15:35:59 +02:00
Fossil
defc448304 Update NZBFinder categories and remove OZnzb & NZB-Tortuga from default definitions (#9474)
NZB Finder will consolidate WEBDL & X265 into SD,HD,UHD so removed 2080 and 2090 categories.

OZnzb and NZB Tortuga are dead so removed it from the presets list.
2023-12-10 15:14:13 +02:00
Bogdan
3ec3358728 Bump version to 5.2.5 2023-12-10 13:47:06 +02:00
239 changed files with 3373 additions and 2141 deletions

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.2.4'
majorVersion: '5.3.1'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.413'
dotnetVersion: '6.0.417'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'

View File

@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
function getIconName(eventType) {
function getIconName(eventType, data) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
@@ -17,7 +17,7 @@ function getIconName(eventType) {
case 'downloadFailed':
return icons.DOWNLOADING;
case 'movieFileDeleted':
return icons.DELETE;
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
case 'movieFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
case 'downloadFailed':
return translate('MovieDownloadFailedTooltip');
case 'movieFileDeleted':
return translate('MovieFileDeletedTooltip');
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip');
case 'downloadIgnored':
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
}
function HistoryEventTypeCell({ eventType, data }) {
const iconName = getIconName(eventType);
const iconName = getIconName(eventType, data);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);

View File

@@ -85,8 +85,13 @@
margin-top: 20px;
}
.studio,
.genres {
margin-left: 5px;
}
.links {
margin-left: 8px;
margin-left: 5px;
pointer-events: all;
}

View File

@@ -5,6 +5,7 @@ interface CssExports {
'certification': string;
'content': string;
'exclusionIcon': string;
'genres': string;
'icons': string;
'links': string;
'overlay': string;
@@ -14,6 +15,7 @@ interface CssExports {
'runtime': string;
'searchResult': string;
'statusContainer': string;
'studio': string;
'title': string;
'titleContainer': string;
'titleRow': string;

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating';
@@ -61,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
titleSlug,
year,
studio,
genres,
status,
overview,
ratings,
@@ -76,6 +78,7 @@ class AddNewMovieSearchResult extends Component {
hasFile,
isAvailable,
movieFile,
queueItem,
runtime,
movieRuntimeFormat,
certification
@@ -197,13 +200,46 @@ class AddNewMovieSearchResult extends Component {
/>
</Label>
{
ratings.imdb ?
<Label size={sizes.LARGE}>
<ImdbRating
ratings={ratings}
iconSize={13}
/>
</Label> :
null
}
{
!!studio &&
<Label size={sizes.LARGE}>
{studio}
<Icon
name={icons.STUDIO}
size={13}
/>
<span className={styles.studio}>
{studio}
</span>
</Label>
}
{
genres.length > 0 ?
<Label size={sizes.LARGE}>
<Icon
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
</Label> :
null
}
<Tooltip
anchor={
<Label
@@ -215,15 +251,15 @@ class AddNewMovieSearchResult extends Component {
/>
<span className={styles.links}>
Links
{translate('Links')}
</span>
</Label>
}
tooltip={
<MovieDetailsLinks
tmdbId={tmdbId}
youTubeTrailerId={youTubeTrailerId}
imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId}
/>
}
canFlip={true}
@@ -237,6 +273,7 @@ class AddNewMovieSearchResult extends Component {
hasMovieFiles={hasFile}
monitored={monitored}
isAvailable={isAvailable}
queueItem={queueItem}
id={id}
useLabel={true}
colorImpairedMode={colorImpairedMode}
@@ -273,6 +310,7 @@ AddNewMovieSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
studio: PropTypes.string,
genres: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired,
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
@@ -283,15 +321,19 @@ AddNewMovieSearchResult.propTypes = {
isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
id: PropTypes.number,
queueItems: PropTypes.arrayOf(PropTypes.object),
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
queueItem: PropTypes.object,
colorImpairedMode: PropTypes.bool,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string
};
AddNewMovieSearchResult.defaultProps = {
genres: []
};
export default AddNewMovieSearchResult;

View File

@@ -10,14 +10,18 @@ function createMapStateToProps() {
createExistingMovieSelector(),
createExclusionMovieSelector(),
createDimensionsSelector(),
(state) => state.queue.details.items,
(state, { internalId }) => internalId,
(state) => state.settings.ui.item.movieRuntimeFormat,
(isExistingMovie, isExclusionMovie, dimensions, internalId, movieRuntimeFormat) => {
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId, movieRuntimeFormat) => {
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
return {
existingMovieId: internalId,
isExistingMovie,
isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen,
queueItem,
movieRuntimeFormat
};
}

View File

@@ -32,7 +32,7 @@
.contentContainer {
z-index: $popperZIndex;
margin-top: 4px;
/* 400px container witdh with 8px padding on each side */
/* 400px container width with 8px padding on each side */
width: 384px;
}

View File

@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
{translate('UnableToAddRootFolder')}
{translate('AddRootFolderError')}
<ul>
{

View File

@@ -44,7 +44,16 @@ export interface CustomFilter {
filers: PropertyFilter[];
}
export interface AppSectionState {
dimensions: {
isSmallScreen: boolean;
width: number;
height: number;
};
}
interface AppState {
app: AppSectionState;
calendar: CalendarAppState;
commands: CommandAppState;
history: HistoryAppState;

View File

@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
gotoCalendarToday
} = this.props;
registerPagePopulator(this.repopulate);
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
if (useCurrentPage) {
fetchCalendar();

View File

@@ -167,7 +167,7 @@ class SignalRConnector extends Component {
const resource = body.resource;
const status = resource.status;
// Both sucessful and failed commands need to be
// Both successful and failed commands need to be
// completed, otherwise they spin until they timeout.
if (status === 'completed' || status === 'failed') {
@@ -187,6 +187,8 @@ class SignalRConnector extends Component {
repopulatePage('movieFileUpdated');
} else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('movieFileDeleted');
}
};

View File

@@ -15,5 +15,5 @@
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "standalone"
"display": "minimal-ui"
}

View File

@@ -59,6 +59,7 @@ import {
faEye as fasEye,
faFastBackward as fasFastBackward,
faFastForward as fasFastForward,
faFileCircleQuestion as fasFileCircleQuestion,
faFileExport as fasFileExport,
faFileInvoice as farFileInvoice,
faFilm as fasFilm,
@@ -159,6 +160,7 @@ export const EXPORT = fasFileExport;
export const EXTERNAL_LINK = fasExternalLinkAlt;
export const FATAL = fasTimesCircle;
export const FILE = farFile;
export const FILE_MISSING = fasFileCircleQuestion;
export const FILM = fasFilm;
export const FILTER = fasFilter;
export const FLAG = fasFlag;

View File

@@ -22,6 +22,10 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.quality {
white-space: nowrap;
}
.languages {
width: 100px;
}

View File

@@ -266,7 +266,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
</TableRowCell>
<TableRowCell className={styles.quality}>
<MovieQuality quality={quality} />
<MovieQuality quality={quality} showRevision={true} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>

View File

@@ -5,6 +5,29 @@ import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster';
function crewSort(a, b) {
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
const indexA = jobOrder.indexOf(a.job);
const indexB = jobOrder.indexOf(b.job);
if (indexA === -1 && indexB === -1) {
return 0;
} else if (indexA === -1) {
return 1;
} else if (indexB === -1) {
return -1;
}
if (indexA < indexB) {
return -1;
} else if (indexA > indexB) {
return 1;
}
return 0;
}
function createMapStateToProps() {
return createSelector(
(state) => state.movieCredits.items,
@@ -17,8 +40,10 @@ function createMapStateToProps() {
return acc;
}, []);
const sortedCrew = crew.sort(crewSort);
return {
items: _.uniqBy(crew, 'personName')
items: _.uniqBy(sortedCrew, 'personName')
};
}
);

View File

@@ -9,3 +9,9 @@
.container {
padding: 10px;
}
.sliderContainer {
--swiper-navigation-color: var(--white);
display: block;
}

View File

@@ -4,6 +4,7 @@ interface CssExports {
'container': string;
'grid': string;
'movie': string;
'sliderContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -14,24 +14,6 @@ import 'swiper/css/navigation';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
const additionalColumnCount = {
small: 3,
medium: 2,
large: 1
};
function calculateColumnWidth(width, posterSize, isSmallScreen) {
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
const columns = Math.floor(width / maxiumColumnWidth);
const remainder = width % maxiumColumnWidth;
if (remainder === 0 && posterSize === 'large') {
return maxiumColumnWidth;
}
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
}
function calculateRowHeight(posterHeight, isSmallScreen) {
const titleHeight = 19;
const characterHeight = 19;
@@ -46,10 +28,6 @@ function calculateRowHeight(posterHeight, isSmallScreen) {
return heights.reduce((acc, height) => acc + height, 0);
}
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class MovieCreditPosters extends Component {
//
@@ -66,39 +44,16 @@ class MovieCreditPosters extends Component {
posterHeight: 238,
rowHeight: calculateRowHeight(238, props.isSmallScreen)
};
this._isInitialized = false;
}
//
// Control
calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - padding;
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
this.setState({
width,
columnWidth,
columnCount,
posterWidth,
posterHeight,
rowHeight
});
};
//
// Render
render() {
const {
items,
itemComponent
itemComponent,
isSmallScreen
} = this.props;
const {
@@ -113,14 +68,13 @@ class MovieCreditPosters extends Component {
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={3}
slidesPerGroup={isSmallScreen ? 1 : 3}
navigation={true}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}

View File

@@ -320,8 +320,8 @@ class MovieDetails extends Component {
/>
<PageToolbarButton
label={translate('ManualImport')}
iconName={icons.INTERACTIVE}
label={translate('ManageFiles')}
iconName={icons.MOVIE_FILE}
onPress={this.onInteractiveImportPress}
/>
@@ -704,6 +704,7 @@ class MovieDetails extends Component {
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
movieId={id}
modalTitle={translate('ManageFiles')}
folder={path}
allowMovieChange={false}
showFilterExistingFiles={true}

View File

@@ -33,14 +33,11 @@ const selectMovieFiles = createSelector(
const hasMovieFiles = !!items.length;
const sizeOnDisk = items.map((item) => item.size).reduce((prev, curr) => prev + curr, 0);
return {
isMovieFilesFetching: isFetching,
isMovieFilesPopulated: isPopulated,
movieFilesError: error,
hasMovieFiles,
sizeOnDisk
hasMovieFiles
};
}
);
@@ -104,8 +101,7 @@ function createMapStateToProps() {
isMovieFilesFetching,
isMovieFilesPopulated,
movieFilesError,
hasMovieFiles,
sizeOnDisk
hasMovieFiles
} = movieFiles;
const {
@@ -161,7 +157,6 @@ function createMapStateToProps() {
movieCreditsError,
extraFilesError,
hasMovieFiles,
sizeOnDisk,
previousMovie,
nextMovie,
isSmallScreen: dimensions.isSmallScreen,

View File

@@ -6,31 +6,43 @@ import MovieTitlesTableContent from './MovieTitlesTableContent';
function createMapStateToProps() {
return createSelector(
(state, { movieId }) => movieId,
(state) => state.movies,
(movies) => {
return movies;
(movieId, movies) => {
const {
isFetching,
isPopulated,
error,
items
} = movies;
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
return {
isFetching,
isPopulated,
error,
alternateTitles
};
}
);
}
const mapDispatchToProps = {
// fetchMovies
};
class MovieTitlesTableContentConnector extends Component {
//
// Render
render() {
const movie = this.props.items.filter((obj) => {
return obj.id === this.props.movieId;
});
const {
alternateTitles,
...otherProps
} = this.props;
return (
<MovieTitlesTableContent
{...this.props}
items={movie[0].alternateTitles}
{...otherProps}
items={alternateTitles}
/>
);
}
@@ -38,7 +50,11 @@ class MovieTitlesTableContentConnector extends Component {
MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
MovieTitlesTableContentConnector.defaultProps = {
alternateTitles: []
};
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);

View File

@@ -56,7 +56,6 @@ const columns = [
},
{
name: 'actions',
label: () => translate('Actions'),
isVisible: true
}
];

View File

@@ -7,8 +7,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
@@ -103,20 +102,11 @@ class MovieHistoryRow extends Component {
</TableRowCell>
<TableRowCell>
<MovieFormats
formats={customFormats}
/>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCellConnector
@@ -134,6 +124,7 @@ class MovieHistoryRow extends Component {
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress}
/>
}

View File

@@ -1,11 +1,12 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
import { MOVIE_SEARCH } from 'Commands/commandNames';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
@@ -21,11 +22,12 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
const {
items,
totalItems,
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
const dispatch = useDispatch();
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const { isSelectMode, selectedFilterKey } = props;
const [selectState] = useSelect();
const { selectedState } = selectState;
@@ -50,6 +52,8 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
: translate('SearchAll');
const onPress = useCallback(() => {
setIsConfirmModalOpen(false);
dispatch(
executeCommand({
name: MOVIE_SEARCH,
@@ -58,14 +62,36 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
);
}, [dispatch, moviesToSearch]);
const onConfirmPress = useCallback(() => {
setIsConfirmModalOpen(true);
}, [setIsConfirmModalOpen]);
const onConfirmModalClose = useCallback(() => {
setIsConfirmModalOpen(false);
}, [setIsConfirmModalOpen]);
return (
<PageToolbarButton
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
isSpinning={isSearching}
isDisabled={!totalItems}
iconName={icons.SEARCH}
onPress={onPress}
/>
<>
<PageToolbarButton
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
isSpinning={isSearching}
isDisabled={!items.length}
iconName={icons.SEARCH}
onPress={moviesToSearch.length > 5 ? onConfirmPress : onPress}
/>
<ConfirmModal
isOpen={isConfirmModalOpen}
kind={kinds.DANGER}
title={isSelectMode ? searchSelectLabel : searchIndexLabel}
message={translate('SearchMoviesConfirmationMessageText', {
count: moviesToSearch.length,
})}
confirmLabel={isSelectMode ? searchSelectLabel : searchIndexLabel}
onConfirm={onPress}
onCancel={onConfirmModalClose}
/>
</>
);
}

View File

@@ -256,13 +256,18 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
if (current) {
const width = current.clientWidth;
const padding = bodyPadding - 5;
const finalWidth = width - padding * 2;
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
return;
}
setSize({
width: width - padding * 2,
width: finalWidth,
height: window.innerHeight,
});
}
}, [isSmallScreen, scrollerRef, bounds]);
}, [isSmallScreen, size, scrollerRef, bounds]);
useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement;

View File

@@ -3,6 +3,7 @@ import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
const revision = quality.revision;
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
return title;
}
function revisionLabel(className, quality, showRevision) {
if (!showRevision) {
return;
}
if (quality.revision.isRepack) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Repack')}
>
R
</Label>
);
}
if (quality.revision.version && quality.revision.version > 1) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Proper')}
>
P
</Label>
);
}
}
function MovieQuality(props) {
const {
className,
@@ -35,7 +66,8 @@ function MovieQuality(props) {
quality,
size,
isMonitored,
isCutoffNotMet
isCutoffNotMet,
showRevision
} = props;
let kind = kinds.DEFAULT;
@@ -50,13 +82,15 @@ function MovieQuality(props) {
}
return (
<Label
className={className}
kind={kind}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
>
{quality.quality.name}
</Label>
<span>
<Label
className={className}
kind={kind}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
>
{quality.quality.name}
</Label>{revisionLabel(className, quality, showRevision)}
</span>
);
}
@@ -66,12 +100,14 @@ MovieQuality.propTypes = {
quality: PropTypes.object.isRequired,
size: PropTypes.number,
isMonitored: PropTypes.bool,
isCutoffNotMet: PropTypes.bool
isCutoffNotMet: PropTypes.bool,
showRevision: PropTypes.bool
};
MovieQuality.defaultProps = {
title: '',
isMonitored: true
isMonitored: true,
showRevision: false
};
export default MovieQuality;

View File

@@ -1,82 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './RootFolderRow.css';
function RootFolderRow(props) {
const {
id,
path,
accessible,
freeSpace,
unmappedFolders,
onDeletePress
} = props;
const isUnavailable = !accessible;
return (
<TableRow>
<TableRowCell>
{
isUnavailable ?
<div className={styles.unavailablePath}>
{path}
<Label
className={styles.unavailableLabel}
kind={kinds.DANGER}
>
{translate('Unavailable')}
</Label>
</div> :
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
}
</TableRowCell>
<TableRowCell className={styles.freeSpace}>
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
</TableRowCell>
<TableRowCell className={styles.unmappedFolders}>
{isUnavailable ? '-' : unmappedFolders.length}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RemoveRootFolder')}
name={icons.REMOVE}
onPress={onDeletePress}
/>
</TableRowCell>
</TableRow>
);
}
RootFolderRow.propTypes = {
id: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
accessible: PropTypes.bool.isRequired,
freeSpace: PropTypes.number,
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired
};
RootFolderRow.defaultProps = {
unmappedFolders: []
};
export default RootFolderRow;

View File

@@ -1,92 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import RootFolderRowConnector from './RootFolderRowConnector';
const rootFolderColumns = [
{
name: 'path',
get label() {
return translate('Path');
},
isVisible: true
},
{
name: 'freeSpace',
get label() {
return translate('FreeSpace');
},
isVisible: true
},
{
name: 'unmappedFolders',
get label() {
return translate('UnmappedFolders');
},
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function RootFolders(props) {
const {
isFetching,
isPopulated,
error,
items
} = props;
if (isFetching && !isPopulated) {
return (
<LoadingIndicator />
);
}
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
</Alert>
);
}
return (
<Table
columns={rootFolderColumns}
>
<TableBody>
{
items.map((rootFolder) => {
return (
<RootFolderRowConnector
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
accessible={rootFolder.accessible}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})
}
</TableBody>
</Table>
);
}
RootFolders.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default RootFolders;

View File

@@ -49,7 +49,7 @@ function RootFolders() {
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert>
<Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert>
);
}

View File

@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteDownloadClients,
bulkEditDownloadClients,
setManageDownloadClientsSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageDownloadClientsModalContent(
@@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
isSaving,
error,
items,
sortKey,
sortDirection,
}: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients')
);
@@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageDownloadClientsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {

View File

@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteIndexers,
bulkEditIndexers,
setManageIndexersSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageIndexersModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isSaving,
error,
items,
sortKey,
sortDirection,
}: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers')
);
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageIndexersSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {

View File

@@ -1,72 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AddRootFolder.css';
class AddRootFolder extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddNewRootFolderModalOpen: false
};
}
//
// Lifecycle
onAddNewRootFolderPress = () => {
this.setState({ isAddNewRootFolderModalOpen: true });
};
onNewRootFolderSelect = ({ value }) => {
this.props.onNewRootFolderSelect(value);
};
onAddRootFolderModalClose = () => {
this.setState({ isAddNewRootFolderModalOpen: false });
};
//
// Render
render() {
return (
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('AddRootFolder')}
</Button>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={this.onNewRootFolderSelect}
onModalClose={this.onAddRootFolderModalClose}
/>
</div>
);
}
}
AddRootFolder.propTypes = {
onNewRootFolderSelect: PropTypes.func.isRequired
};
export default AddRootFolder;

View File

@@ -1,14 +1,18 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props';
import { addRootFolder } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import translate from 'Utilities/String/translate';
import styles from './AddRootFolder.css';
function AddRootFolder() {
const { isSaving, saveError } = useSelector(createRootFoldersSelector());
const dispatch = useDispatch();
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
@@ -30,24 +34,42 @@ function AddRootFolder() {
}, [setIsAddNewRootFolderModalOpen]);
return (
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<>
{!isSaving && saveError ? (
<Alert kind={kinds.DANGER}>
{translate('AddRootFolderError')}
<FileBrowserModal
isOpen={isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={onNewRootFolderSelect}
onModalClose={onAddRootFolderModalClose}
/>
</div>
<ul>
{Array.isArray(saveError.responseJSON) ? (
saveError.responseJSON.map((e, index) => {
return <li key={index}>{e.errorMessage}</li>;
})
) : (
<li>{JSON.stringify(saveError.responseJSON)}</li>
)}
</ul>
</Alert>
) : null}
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<FileBrowserModal
isOpen={isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={onNewRootFolderSelect}
onModalClose={onAddRootFolderModalClose}
/>
</div>
</>
);
}

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -31,9 +33,9 @@ export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadCl
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
//
// Action Creators
@@ -48,9 +50,9 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return {
@@ -90,7 +92,9 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
},
//
@@ -124,7 +128,10 @@ export default {
return selectedSchema;
});
}
},
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
@@ -35,9 +37,9 @@ export const DELETE_INDEXER = 'settings/indexers/deleteIndexer';
export const TEST_INDEXER = 'settings/indexers/testIndexer';
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
//
// Action Creators
@@ -53,9 +55,9 @@ export const deleteIndexer = createThunk(DELETE_INDEXER);
export const testIndexer = createThunk(TEST_INDEXER);
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
return {
@@ -95,7 +97,9 @@ export default {
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
},
//
@@ -157,7 +161,10 @@ export default {
};
return updateSectionState(state, section, newState);
}
},
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@@ -130,7 +130,10 @@ export const actionHandlers = handleThunks({
promise.done((data) => {
const updatedItem = _.cloneDeep(data);
updatedItem.internalId = updatedItem.id;
updatedItem.id = updatedItem.tmdbId;
delete updatedItem.images;
const actions = [
updateItem({ section: 'movies', ...data }),
updateItem({ section: 'addMovie', ...updatedItem }),

View File

@@ -209,7 +209,7 @@ export const defaultState = {
{
name: 'tags',
label: () => translate('Tags'),
isSortable: false,
isSortable: true,
isVisible: false
},
{

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createDimensionsSelector() {
return createSelector(
(state) => state.app.dimensions,
(state: AppState) => state.app.dimensions,
(dimensions) => {
return dimensions;
}

View File

@@ -28,7 +28,7 @@
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.21",
"@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.16.8",

View File

@@ -1,6 +1,9 @@
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Localization;
using NzbDrone.Test.Common;
using Radarr.Http.ClientSchema;
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
[TestFixture]
public class SchemaBuilderFixture : TestBase
{
[SetUp]
public void Setup()
{
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
.Returns<string, Dictionary<string, object>>((s, d) => s);
SchemaBuilder.Initialize(Mocker.Container);
}
[Test]
public void should_return_field_for_every_property()
{

View File

@@ -47,7 +47,7 @@ namespace NzbDrone.Common.Test.Http
// Use mirrors for tests that use two hosts
var candidates = new[] { "httpbin1.servarr.com" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
// httpbin.org is broken right now, occasionally redirecting to https if it's unavailable.
_httpBinHost = mainHost;
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();

View File

@@ -67,7 +67,7 @@ namespace NzbDrone.Common.EnvironmentInfo
}
catch (Exception ex)
{
_logger.Warn(ex, "Coudn't set app folder permission");
_logger.Warn(ex, "Couldn't set app folder permission");
}
}

View File

@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else if (value is Dictionary<string, string> d)
{
data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value))));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");

View File

@@ -123,7 +123,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
_debounce = new SentryDebounce();
// initialize to true and reconfigure later
// Otherwise it will default to false and any errors occuring
// Otherwise it will default to false and any errors occurring
// before config file gets read will not be filtered
FilterEvents = true;
SentryEnabled = true;

View File

@@ -260,7 +260,7 @@ namespace NzbDrone.Common.OAuth
}
/// <summary>
/// Creates a request elements concatentation value to send with a request.
/// Creates a request elements concatenation value to send with a request.
/// This is also known as the signature base.
/// </summary>
/// <seealso href="http://oauth.net/core/1.0#rfc.section.9.1.3"/>

View File

@@ -4,17 +4,17 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3" />
<PackageReference Include="Npgsql" Version="7.0.4" />
<PackageReference Include="Npgsql" Version="7.0.6" />
<PackageReference Include="Sentry" Version="3.23.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.8" />
<PackageReference Include="System.Text.Json" Version="6.0.9" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />

View File

@@ -215,7 +215,7 @@ namespace NzbDrone.Common
if (dacls.Contains(authenticatedUsersDacl))
{
// Permssions already set
// Permissions already set
return;
}

View File

@@ -4,6 +4,7 @@ using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.ImportListMovies;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
@@ -42,8 +43,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
GivenImportList();
var movieMetadata = Builder<MovieMetadata>.CreateNew().BuildNew();
Db.Insert(movieMetadata);
var status = Builder<ImportListMovie>.CreateNew()
.With(h => h.ListId = _importList.Id)
.With(b => b.MovieMetadataId = movieMetadata.Id)
.BuildNew();
Db.Insert(status);

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests;
[TestFixture]
public class IndexerBaseFixture : CoreTest<IndexerBase<TestIndexerSettings>>
{
private TestIndexer _indexer;
[SetUp]
public void Setup()
{
_indexer = new TestIndexer(new Mock<IHttpClient>().Object,
new Mock<IIndexerStatusService>().Object,
new Mock<IConfigService>().Object,
new Mock<IParsingService>().Object,
new Mock<Logger>().Object)
{
Definition = new IndexerDefinition
{
Settings = new TestIndexerSettings
{
MultiLanguages = new List<int> { Language.German.Id, Language.English.Id }
}
}
};
}
[TestCase("The.Movie.Name.2016.Multi.DTS.720p.BluRay.x264-RlsGrp")]
public void should_parse_multi_language(string postTitle)
{
var result = _indexer.CleanupReleases(new ReleaseInfo[] { new () { Title = postTitle, Languages = new List<Language>() } });
result.Single().Languages.Count.Should().Be(2);
result.Single().Languages.Should().Contain(Language.German);
result.Single().Languages.Should().Contain(Language.English);
}
}

View File

@@ -1,8 +1,10 @@
using NLog;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Test.IndexerTests
{
@@ -31,5 +33,10 @@ namespace NzbDrone.Core.Test.IndexerTests
{
return _parser;
}
public new IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
{
return base.CleanupReleases(releases);
}
}
}

View File

@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestCase(HdrFormat.Hdr10Plus, "HDR10Plus")]
[TestCase(HdrFormat.DolbyVision, "DV")]
[TestCase(HdrFormat.DolbyVisionHdr10, "DV HDR10")]
[TestCase(HdrFormat.DolbyVisionHdr10Plus, "DV HDR10Plus")]
[TestCase(HdrFormat.DolbyVisionHlg, "DV HLG")]
[TestCase(HdrFormat.DolbyVisionSdr, "DV SDR")]
public void should_format_video_dynamic_range_type(HdrFormat format, string expectedVideoDynamicRangeType)

View File

@@ -116,6 +116,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 1, HdrFormat.DolbyVisionHdr10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 1, HdrFormat.DolbyVisionHdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 6, HdrFormat.DolbyVisionHdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 2, HdrFormat.DolbyVisionSdr)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 4, HdrFormat.DolbyVisionHlg)]
public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, int? doviConfigId, HdrFormat expected)

View File

@@ -48,13 +48,14 @@ namespace NzbDrone.Core.Test.NotificationTests
Subject.Definition = _traktDefinition;
}
private void GiventValidMediaInfo(Quality quality, string audioChannels, string audioFormat, string scanType)
private void GiventValidMediaInfo(Quality quality, string audioChannels, string audioFormat, string scanType, HdrFormat hdrFormat = HdrFormat.None)
{
_downloadMessage.MovieFile.MediaInfo = new MediaInfoModel
{
AudioChannelPositions = audioChannels,
AudioFormat = audioFormat,
ScanType = scanType
ScanType = scanType,
VideoHdrFormat = hdrFormat
};
_downloadMessage.MovieFile.Quality.Quality = quality;
@@ -72,7 +73,7 @@ namespace NzbDrone.Core.Test.NotificationTests
[Test]
public void should_add_collection_movie_if_valid_mediainfo()
{
GiventValidMediaInfo(Quality.Bluray1080p, "5.1", "DTS", "Progressive");
GiventValidMediaInfo(Quality.Bluray2160p, "5.1", "DTS", "Progressive", HdrFormat.DolbyVisionHdr10);
Subject.OnDownload(_downloadMessage);
@@ -80,15 +81,16 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "5.1" &&
t.Movies.First().Resolution == "hd_1080p" &&
t.Movies.First().MediaType == "bluray"),
t.Movies.First().Resolution == "uhd_4k" &&
t.Movies.First().MediaType == "bluray" &&
t.Movies.First().Hdr == "hdr10"),
It.IsAny<string>()), Times.Once());
}
[Test]
public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie()
{
GiventValidMediaInfo(Quality.Bluray1080p, "2.0", "DTS", "Progressive");
GiventValidMediaInfo(Quality.Bluray2160p, "2.0", "DTS", "Progressive", HdrFormat.DolbyVisionHdr10);
Subject.OnDownload(_downloadMessage);
@@ -96,8 +98,9 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "2.0" &&
t.Movies.First().Resolution == "hd_1080p" &&
t.Movies.First().MediaType == "bluray"),
t.Movies.First().Resolution == "uhd_4k" &&
t.Movies.First().MediaType == "bluray" &&
t.Movies.First().Hdr == "hdr10"),
It.IsAny<string>()), Times.Once());
}
}

View File

@@ -140,6 +140,8 @@ namespace NzbDrone.Core.Test.ParserTests
}
[TestCase("Movie.Title.1994.Russian.1080p.XviD-LOL")]
[TestCase("Movie.Title.2020.WEB-DLRip.AVC.AC3.EN.RU.ENSub.RUSub-LOL")]
[TestCase("Movie Title (2020) WEB-DL (720p) Rus-Eng")]
public void should_parse_language_russian(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -300,6 +302,7 @@ namespace NzbDrone.Core.Test.ParserTests
}
[TestCase("Movie.Title.1994.Hebrew.1080p.XviD-LOL")]
[TestCase("Movie.Title.1994.1080p.BluRay.HebDubbed.Also.English.x264-P2P")]
public void should_parse_language_hebrew(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -387,6 +390,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Title.2022.lv.WEBRip.XviD-LOL")]
[TestCase("Movie.Title.2022.LATVIAN.WEBRip.XviD-LOL")]
[TestCase("Movie.Title.2022.Latvian.WEBRip.XviD-LOL")]
[TestCase("Movie.Title.2022.1080p.WEB-DL.DDP5.1.Atmos.H.264.Lat.Eng")]
[TestCase("Movie.Title.2022.1080p.WEB-DL.LAV.RUS-NPPK")]
public void should_parse_language_latvian(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
@@ -429,5 +434,35 @@ namespace NzbDrone.Core.Test.ParserTests
var result = LanguageParser.ParseSubtitleLanguage(fileName);
result.Should().Be(Language.Unknown);
}
[TestCase("The.Movie.Name.2016.German.DTS.DL.720p.BluRay.x264-RlsGrp")]
public void should_add_original_language_to_german_release_with_dl_tag(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
result.Languages.Count.Should().Be(2);
result.Languages.Should().Contain(Language.German);
result.Languages.Should().Contain(Language.Original);
}
[TestCase("The.Movie.Name.2016.GERMAN.WEB-DL.h264-RlsGrp")]
[TestCase("The.Movie.Name.2016.GERMAN.WEB.DL.h264-RlsGrp")]
[TestCase("The Movie Name 2016 GERMAN WEB DL h264-RlsGrp")]
[TestCase("The.Movie.Name.2016.GERMAN.WEBDL.h264-RlsGrp")]
public void should_not_add_original_language_to_german_release_when_title_contains_web_dl(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
result.Languages.Count.Should().Be(1);
result.Languages.Should().Contain(Language.German);
}
[TestCase("The.Movie.Name.2023.German.ML.EAC3.720p.NF.WEB.H264-RlsGrp")]
public void should_add_original_language_and_english_to_german_release_with_ml_tag(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle);
result.Languages.Count.Should().Be(3);
result.Languages.Should().Contain(Language.German);
result.Languages.Should().Contain(Language.Original);
result.Languages.Should().Contain(Language.English);
}
}
}

View File

@@ -62,6 +62,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("G.I.Movie.Movie.2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "G.I. Movie Movie")]
[TestCase("www.Torrenting.org - Movie.2008.720p.X264-DIMENSION", "Movie")]
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]", "The French Movie")]
[TestCase("The.Good.German.2006.720p.BluRay.x264-RlsGrp", "The Good German", Description = "Hardcoded to exclude from German regex")]
public void should_parse_movie_title(string postTitle, string title)
{
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(title);
@@ -124,6 +125,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Die.fantastische.Reise.des.Dr.Dolittle.2020.German.DL.LD.1080p.WEBRip.x264-PRD", "Die fantastische Reise des Dr. Dolittle", "", 2020, Description = "dot after dr")]
[TestCase("Der.Film.deines.Lebens.German.2011.PAL.DVDR-ETM", "Der Film deines Lebens", "", 2011, Description = "year at wrong position")]
[TestCase("Kick.Ass.2.2013.German.DTS.DL.720p.BluRay.x264-Pate_", "Kick Ass 2", "", 2013, Description = "underscore at the end")]
[TestCase("The.Good.German.2006.GERMAN.720p.HDTV.x264-RLsGrp", "The Good German", "", 2006, Description = "German in the title")]
public void should_parse_german_movie(string postTitle, string title, string edition, int year)
{
var movie = Parser.Parser.ParseMovieTitle(postTitle);
@@ -238,6 +240,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Italian.Movie.2025.720p.BluRay.X264-AMIABLE")]
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]")]
[TestCase("The.German.Doctor.2013.LIMITED.DVDRip.x264-RedBlade", Description = "When German is not followed by a year or a SCENE word it is not matched")]
[TestCase("The.Good.German.2006.720p.HDTV.x264-TVP", Description = "The Good German is hardcoded not to match")]
[TestCase("German.Lancers.2019.720p.BluRay.x264-UNiVERSUM", Description = "German at the beginning is never matched")]
[TestCase("The.German.2019.720p.BluRay.x264-UNiVERSUM", Description = "The German is hardcoded not to match")]
public void should_not_parse_wrong_language_in_title(string postTitle)
{
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -245,22 +251,6 @@ namespace NzbDrone.Core.Test.ParserTests
parsed.Languages.First().Should().Be(Language.Unknown);
}
[TestCase("The.Movie.Name.2016.German.DTS.DL.720p.BluRay.x264-MULTiPLEX")]
public void should_not_parse_multi_language_in_releasegroup(string postTitle)
{
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
parsed.Languages.Count.Should().Be(1);
parsed.Languages.First().Should().Be(Language.German);
}
[TestCase("The.Movie.Name.2016.German.Multi.DTS.DL.720p.BluRay.x264-MULTiPLEX")]
public void should_parse_multi_language(string postTitle)
{
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
parsed.Languages.Count.Should().Be(1);
parsed.Languages.Should().Contain(Language.German);
}
[TestCase("Movie.Title.2016.1080p.KORSUB.WEBRip.x264.AAC2.0-RADARR", "KORSUB")]
[TestCase("Movie.Title.2016.1080p.KORSUBS.WEBRip.x264.AAC2.0-RADARR", "KORSUBS")]
[TestCase("Movie Title 2017 HC 720p HDRiP DD5 1 x264-LEGi0N", "Generic Hardcoded Subs")]

View File

@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
public string Hint { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class FieldTokenAttribute : Attribute
{
public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null)
{
Label = label;
Field = field;
Token = token;
Value = value?.ToString();
}
public string Label { get; set; }
public TokenField Field { get; set; }
public string Token { get; set; }
public string Value { get; set; }
}
public class FieldSelectOption
{
public int Value { get; set; }
@@ -84,4 +101,11 @@ namespace NzbDrone.Core.Annotations
ApiKey,
UserName
}
public enum TokenField
{
Label,
HelpText,
HelpTextWarning
}
}

View File

@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
public interface IBlocklistService
{
bool Blocklisted(int movieId, ReleaseInfo release);
bool BlocklistedTorrentHash(int movieId, string hash);
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
List<Blocklist> GetByMovieId(int movieId);
void Block(RemoteMovie remoteMovie, string message);
@@ -37,30 +38,34 @@ namespace NzbDrone.Core.Blocklisting
public bool Blocklisted(int movieId, ReleaseInfo release)
{
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(movieId, release.Title);
if (release.DownloadProtocol == DownloadProtocol.Torrent)
{
var torrentInfo = release as TorrentInfo;
if (torrentInfo == null)
if (release is not TorrentInfo torrentInfo)
{
return false;
}
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
{
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
.Any(b => SameTorrent(b, torrentInfo));
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash);
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
}
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash);
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
.Where(b => b.Protocol == DownloadProtocol.Torrent)
.Any(b => SameTorrent(b, torrentInfo));
}
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
.Any(b => SameNzb(b, release));
return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
.Where(b => b.Protocol == DownloadProtocol.Usenet)
.Any(b => SameNzb(b, release));
}
public bool BlocklistedTorrentHash(int movieId, string hash)
{
return _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, hash).Any(b =>
b.TorrentInfoHash.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
}
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)

View File

@@ -330,8 +330,8 @@ namespace NzbDrone.Core.Configuration
return;
}
// If SSL is enabled and a cert hash is still in the config file disable SSL
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
{
SetValue("EnableSsl", false);
}

View File

@@ -0,0 +1,41 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats
{
public class YearSpecificationValidator : AbstractValidator<YearSpecification>
{
public YearSpecificationValidator()
{
RuleFor(c => c.Min).NotEmpty().GreaterThan(0);
RuleFor(c => c.Max).NotEmpty().GreaterThanOrEqualTo(c => c.Min);
}
}
public class YearSpecification : CustomFormatSpecificationBase
{
private static readonly YearSpecificationValidator Validator = new ();
public override int Order => 10;
public override string ImplementationName => "Year";
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)]
public int Min { get; set; }
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)]
public int Max { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
{
var year = input.MovieInfo?.Year ?? input.Movie?.MovieMetadata?.Value?.Year;
return year >= Min && year <= Max;
}
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -9,8 +9,8 @@ namespace NzbDrone.Core.Datastore
{
public interface IConnectionStringFactory
{
string MainDbConnectionString { get; }
string LogDbConnectionString { get; }
DatabaseConnectionInfo MainDbConnection { get; }
DatabaseConnectionInfo LogDbConnection { get; }
string GetDatabasePath(string connectionString);
}
@@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore
{
_configFileProvider = configFileProvider;
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
GetConnectionString(appFolderInfo.GetLogDatabase());
}
public string MainDbConnectionString { get; private set; }
public string LogDbConnectionString { get; private set; }
public DatabaseConnectionInfo MainDbConnection { get; private set; }
public DatabaseConnectionInfo LogDbConnection { get; private set; }
public string GetDatabasePath(string connectionString)
{
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.DataSource;
}
private static string GetConnectionString(string dbPath)
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
{
var connectionBuilder = new SQLiteConnectionStringBuilder
{
@@ -57,21 +57,22 @@ namespace NzbDrone.Core.Datastore
connectionBuilder.Add("Full FSync", true);
}
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString);
}
private string GetPostgresConnectionString(string dbName)
private DatabaseConnectionInfo GetPostgresConnectionString(string dbName)
{
var connectionBuilder = new NpgsqlConnectionStringBuilder();
var connectionBuilder = new NpgsqlConnectionStringBuilder
{
Database = dbName,
Host = _configFileProvider.PostgresHost,
Username = _configFileProvider.PostgresUser,
Password = _configFileProvider.PostgresPassword,
Port = _configFileProvider.PostgresPort,
Enlist = false
};
connectionBuilder.Database = dbName;
connectionBuilder.Host = _configFileProvider.PostgresHost;
connectionBuilder.Username = _configFileProvider.PostgresUser;
connectionBuilder.Password = _configFileProvider.PostgresPassword;
connectionBuilder.Port = _configFileProvider.PostgresPort;
connectionBuilder.Enlist = false;
return connectionBuilder.ConnectionString;
return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace NzbDrone.Core.Datastore
{
public class DatabaseConnectionInfo
{
public DatabaseConnectionInfo(DatabaseType databaseType, string connectionString)
{
DatabaseType = databaseType;
ConnectionString = connectionString;
}
public DatabaseType DatabaseType { get; internal set; }
public string ConnectionString { get; internal set; }
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Data.Common;
using System.Data.SQLite;
using System.Net.Sockets;
using System.Threading;
using NLog;
using Npgsql;
using NzbDrone.Common.Disk;
@@ -60,22 +61,22 @@ namespace NzbDrone.Core.Datastore
public IDatabase Create(MigrationContext migrationContext)
{
string connectionString;
DatabaseConnectionInfo connectionInfo;
switch (migrationContext.MigrationType)
{
case MigrationType.Main:
{
connectionString = _connectionStringFactory.MainDbConnectionString;
CreateMain(connectionString, migrationContext);
connectionInfo = _connectionStringFactory.MainDbConnection;
CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break;
}
case MigrationType.Log:
{
connectionString = _connectionStringFactory.LogDbConnectionString;
CreateLog(connectionString, migrationContext);
connectionInfo = _connectionStringFactory.LogDbConnection;
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break;
}
@@ -90,14 +91,14 @@ namespace NzbDrone.Core.Datastore
{
DbConnection conn;
if (connectionString.Contains(".db"))
if (connectionInfo.DatabaseType == DatabaseType.SQLite)
{
conn = SQLiteFactory.Instance.CreateConnection();
conn.ConnectionString = connectionString;
conn.ConnectionString = connectionInfo.ConnectionString;
}
else
{
conn = new NpgsqlConnection(connectionString);
conn = new NpgsqlConnection(connectionInfo.ConnectionString);
}
conn.Open();
@@ -107,12 +108,12 @@ namespace NzbDrone.Core.Datastore
return db;
}
private void CreateMain(string connectionString, MigrationContext migrationContext)
private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
try
{
_restoreDatabaseService.Restore();
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (SQLiteException e)
{
@@ -135,15 +136,17 @@ namespace NzbDrone.Core.Datastore
{
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
Thread.Sleep(5000);
try
{
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
return;
}
catch (Exception ex)
{
if (--retryCount > 0)
{
System.Threading.Thread.Sleep(5000);
continue;
}
@@ -162,11 +165,11 @@ namespace NzbDrone.Core.Datastore
}
}
private void CreateLog(string connectionString, MigrationContext migrationContext)
private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
try
{
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (SQLiteException e)
{
@@ -186,7 +189,7 @@ namespace NzbDrone.Core.Datastore
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
}
_migrationController.Migrate(connectionString, migrationContext);
_migrationController.Migrate(connectionString, migrationContext, databaseType);
}
catch (Exception e)
{

View File

@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Datastore
}
/// <summary>
/// Visits the memeber access expression. To be implemented by user.
/// Visits the member access expression. To be implemented by user.
/// </summary>
/// <param name="expression"></param>
/// <returns></returns>

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(234)]
public class movie_last_searched_time : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Movies").AddColumn("LastSearchTime").AsDateTimeOffset().Nullable();
}
}
}

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{
public interface IMigrationController
{
void Migrate(string connectionString, MigrationContext migrationContext);
void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType);
}
public class MigrationController : IMigrationController
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_migrationLoggerProvider = migrationLoggerProvider;
}
public void Migrate(string connectionString, MigrationContext migrationContext)
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{
var sw = Stopwatch.StartNew();
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
ServiceProvider serviceProvider;
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
serviceProvider = new ServiceCollection()
.AddLogging(b => b.AddNLog())

View File

@@ -108,7 +108,7 @@ namespace NzbDrone.Core.DecisionEngine
private int ComparePeersIfTorrent(DownloadDecision x, DownloadDecision y)
{
// Different protocols should get caught when checking the preferred protocol,
// since we're dealing with the same movie in our comparisions
// since we're dealing with the same movie in our comparisons
if (x.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent ||
y.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent)
{

View File

@@ -78,7 +78,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return false;
}
_logger.Debug("New item has a better custom format score");
_logger.Debug("New item's custom formats [{0}] ({1}) improve on [{2}] ({3}), accepting",
newCustomFormats.ConcatToString(),
newFormatScore,
currentCustomFormats.ConcatToString(),
currentFormatScore);
return true;
}

View File

@@ -7,9 +7,9 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -26,11 +26,11 @@ namespace NzbDrone.Core.Download.Clients.Aria2
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;
}

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
@@ -97,8 +98,14 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public string AddMagnet(Aria2Settings settings, string magnet)
{
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet });
var options = new Dictionary<string, string>();
if (settings.Directory.IsNotNullOrWhiteSpace())
{
options.Add("dir", settings.Directory);
}
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet }, options);
var gid = response.GetStringResponse();
return gid;
@@ -106,8 +113,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent);
// Aria2's second parameter is an array of URIs and needs to be sent if options are provided, this satisfies that requirement.
var emptyListOfUris = new List<string>();
var options = new Dictionary<string, string>();
if (settings.Directory.IsNotNullOrWhiteSpace())
{
options.Add("dir", settings.Directory);
}
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent, emptyListOfUris, options);
var gid = response.GetStringResponse();
return gid;

View File

@@ -40,6 +40,10 @@ namespace NzbDrone.Core.Download.Clients.Aria2
[FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string SecretToken { get; set; }
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")]
public string Directory { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -23,15 +23,13 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
private readonly Logger _logger;
private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService;
private readonly INamingConfigService _namingConfigService;
private readonly ICached<Dictionary<string, WatchFolderItem>> _watchFolderItemCache;
public ScanWatchFolder(ICacheManager cacheManager, IDiskScanService diskScanService, INamingConfigService namingConfigService, IDiskProvider diskProvider, Logger logger)
public ScanWatchFolder(ICacheManager cacheManager, IDiskScanService diskScanService, IDiskProvider diskProvider, Logger logger)
{
_logger = logger;
_diskProvider = diskProvider;
_diskScanService = diskScanService;
_namingConfigService = namingConfigService;
_watchFolderItemCache = cacheManager.GetCache<Dictionary<string, WatchFolderItem>>(GetType());
}

View File

@@ -7,6 +7,7 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
@@ -27,11 +28,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_scanWatchFolder = scanWatchFolder;

View File

@@ -22,12 +22,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
public UsenetBlackhole(IScanWatchFolder scanWatchFolder,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService,
Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{
_scanWatchFolder = scanWatchFolder;

View File

@@ -7,9 +7,9 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -24,11 +24,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;
}

View File

@@ -8,10 +8,10 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
@@ -35,11 +35,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_dsInfoProxy = dsInfoProxy;
_dsTaskProxySelector = dsTaskProxySelector;

View File

@@ -9,7 +9,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
@@ -32,12 +31,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
IDownloadStationTaskProxySelector dsTaskProxySelector,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService,
Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{
_dsInfoProxy = dsInfoProxy;
_dsTaskProxySelector = dsTaskProxySelector;

View File

@@ -6,10 +6,10 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Flood.Models;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
@@ -26,11 +26,11 @@ namespace NzbDrone.Core.Download.Clients.Flood
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;
_downloadSeedConfigProvider = downloadSeedConfigProvider;
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
}
}
return result;
return result.Where(t => t.IsNotNullOrWhiteSpace());
}
public override string Name => "Flood";

View File

@@ -3,14 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
@@ -21,15 +20,14 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
private readonly IFreeboxDownloadProxy _proxy;
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
ICacheManager cacheManager,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;
}

View File

@@ -6,10 +6,10 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Hadouken.Models;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -24,11 +24,11 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;
}

View File

@@ -8,7 +8,6 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -22,12 +21,11 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
public NzbVortex(INzbVortexProxy proxy,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService,
Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{
_proxy = proxy;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Net;
using Newtonsoft.Json;

View File

@@ -10,7 +10,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -26,12 +25,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
public Nzbget(INzbgetProxy proxy,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService,
Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{
_proxy = proxy;
}

View File

@@ -21,11 +21,10 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
public Pneumatic(IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(configService, diskProvider, remotePathMappingService, logger)
{
_httpClient = httpClient;
}

View File

@@ -8,9 +8,9 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -32,12 +32,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
ICacheManager cacheManager,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxySelector = proxySelector;

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentContentLayout
{
Default = 0,
Original = 1,
Subfolder = 2
}
}

View File

@@ -265,6 +265,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
request.AddFormParameter("firstLastPiecePrio", true);
}
if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Original)
{
request.AddFormParameter("contentLayout", "Original");
}
else if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Subfolder)
{
request.AddFormParameter("contentLayout", "Subfolder");
}
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)

View File

@@ -69,6 +69,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(12, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
public bool FirstAndLast { get; set; }
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
public int ContentLayout { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -10,7 +10,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -24,12 +23,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public Sabnzbd(ISabnzbdProxy proxy,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService,
Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{
_proxy = proxy;
}

View File

@@ -4,9 +4,9 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Transmission
@@ -17,11 +17,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
}

View File

@@ -6,9 +6,9 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -23,11 +23,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;
}

View File

@@ -141,7 +141,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private TransmissionResponse GetSessionVariables(TransmissionSettings settings)
{
// Retrieve transmission information such as the default download directory, bandwith throttling and seed ratio.
// Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio.
return ProcessRequest("session-get", null, settings);
}

View File

@@ -2,10 +2,10 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Transmission;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Vuze
@@ -18,11 +18,11 @@ namespace NzbDrone.Core.Download.Clients.Vuze
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
}

View File

@@ -8,11 +8,11 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.rTorrent;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
@@ -31,13 +31,13 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IDownloadSeedConfigProvider downloadSeedConfigProvider,
IRTorrentDirectoryValidator rTorrentDirectoryValidator,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;

View File

@@ -8,9 +8,9 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
@@ -27,11 +27,11 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{
_proxy = proxy;

View File

@@ -1,16 +1,19 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using Polly;
using Polly.Retry;
namespace NzbDrone.Core.Download
{
@@ -18,11 +21,41 @@ namespace NzbDrone.Core.Download
where TSettings : IProviderConfig, new()
{
protected readonly IConfigService _configService;
protected readonly INamingConfigService _namingConfigService;
protected readonly IDiskProvider _diskProvider;
protected readonly IRemotePathMappingService _remotePathMappingService;
protected readonly Logger _logger;
protected ResiliencePipeline<HttpResponse> RetryStrategy => new ResiliencePipelineBuilder<HttpResponse>()
.AddRetry(new RetryStrategyOptions<HttpResponse>
{
ShouldHandle = static args => args.Outcome switch
{
{ Result.HasHttpServerError: true } => PredicateResult.True(),
{ Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(),
_ => PredicateResult.False()
},
Delay = TimeSpan.FromSeconds(3),
MaxRetryAttempts = 2,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
var exception = args.Outcome.Exception;
if (exception is not null)
{
_logger.Info(exception, "Request for {0} failed with exception '{1}'. Retrying in {2}s.", Definition.Name, exception.Message, args.RetryDelay.TotalSeconds);
}
else
{
_logger.Info("Request for {0} failed with status {1}. Retrying in {2}s.", Definition.Name, args.Outcome.Result?.StatusCode, args.RetryDelay.TotalSeconds);
}
return default;
}
})
.Build();
public abstract string Name { get; }
public Type ConfigContract => typeof(TSettings);
@@ -41,13 +74,11 @@ namespace NzbDrone.Core.Download
protected TSettings Settings => (TSettings)Definition.Settings;
protected DownloadClientBase(IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
Logger logger)
{
_configService = configService;
_namingConfigService = namingConfigService;
_diskProvider = diskProvider;
_remotePathMappingService = remotePathMappingService;
_logger = logger;
@@ -58,10 +89,7 @@ namespace NzbDrone.Core.Download
return GetType().Name;
}
public abstract DownloadProtocol Protocol
{
get;
}
public abstract DownloadProtocol Protocol { get; }
public abstract Task<string> Download(RemoteMovie remoteMovie, IIndexer indexer);
public abstract IEnumerable<DownloadClientItem> GetItems();

View File

@@ -103,6 +103,11 @@ namespace NzbDrone.Core.Download
_logger.Trace("Release {0} no longer available on indexer.", remoteMovie);
throw;
}
catch (ReleaseBlockedException)
{
_logger.Trace("Release {0} previously added to blocklist, not sending to download client again.", remoteMovie);
throw;
}
catch (DownloadClientRejectedReleaseException)
{
_logger.Trace("Release {0} rejected by download client, possible duplicate.", remoteMovie);
@@ -127,7 +132,7 @@ namespace NzbDrone.Core.Download
movieGrabbedEvent.DownloadClientId = downloadClient.Definition.Id;
movieGrabbedEvent.DownloadClientName = downloadClient.Definition.Name;
if (!string.IsNullOrWhiteSpace(downloadClientId))
if (downloadClientId.IsNotNullOrWhiteSpace())
{
movieGrabbedEvent.DownloadId = downloadClientId;
}

View File

@@ -129,7 +129,7 @@ namespace NzbDrone.Core.Download
private void PublishDownloadFailedEvent(List<MovieHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
{
var historyItem = historyItems.First();
var historyItem = historyItems.Last();
Enum.TryParse(historyItem.Data.GetValueOrDefault(MovieHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
var downloadFailedEvent = new DownloadFailedEvent

View File

@@ -326,7 +326,8 @@ namespace NzbDrone.Core.Download.Pending
Reason = reason,
AdditionalInfo = new PendingReleaseAdditionalInfo
{
MovieMatchType = decision.RemoteMovie.MovieMatchType
MovieMatchType = decision.RemoteMovie.MovieMatchType,
ReleaseSource = decision.RemoteMovie.ReleaseSource
}
};

View File

@@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers;
@@ -21,18 +22,20 @@ namespace NzbDrone.Core.Download
where TSettings : IProviderConfig, new()
{
protected readonly IHttpClient _httpClient;
private readonly IBlocklistService _blocklistService;
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger)
: base(configService, namingConfigService, diskProvider, remotePathMappingService, logger)
: base(configService, diskProvider, remotePathMappingService, logger)
{
_httpClient = httpClient;
_blocklistService = blocklistService;
_torrentFileInfoReader = torrentFileInfoReader;
}
@@ -87,7 +90,7 @@ namespace NzbDrone.Core.Download
{
try
{
return DownloadFromMagnetUrl(remoteMovie, magnetUrl);
return DownloadFromMagnetUrl(remoteMovie, indexer, magnetUrl);
}
catch (NotSupportedException ex)
{
@@ -101,7 +104,7 @@ namespace NzbDrone.Core.Download
{
try
{
return DownloadFromMagnetUrl(remoteMovie, magnetUrl);
return DownloadFromMagnetUrl(remoteMovie, indexer, magnetUrl);
}
catch (NotSupportedException ex)
{
@@ -134,7 +137,9 @@ namespace NzbDrone.Core.Download
request.Headers.Accept = "application/x-bittorrent";
request.AllowAutoRedirect = false;
var response = await _httpClient.GetAsync(request);
var response = await RetryStrategy
.ExecuteAsync(static async (state, _) => await state._httpClient.GetAsync(state.request), (_httpClient, request))
.ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
response.StatusCode == HttpStatusCode.Found ||
@@ -148,7 +153,7 @@ namespace NzbDrone.Core.Download
{
if (locationHeader.StartsWith("magnet:"))
{
return DownloadFromMagnetUrl(remoteMovie, locationHeader);
return DownloadFromMagnetUrl(remoteMovie, indexer, locationHeader);
}
request.Url += new HttpUri(locationHeader);
@@ -191,6 +196,9 @@ namespace NzbDrone.Core.Download
var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteMovie.Release.Title));
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
EnsureReleaseIsNotBlocklisted(remoteMovie, indexer, hash);
var actualHash = AddFromTorrentFile(remoteMovie, hash, filename, torrentFile);
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
@@ -204,7 +212,7 @@ namespace NzbDrone.Core.Download
return actualHash;
}
private string DownloadFromMagnetUrl(RemoteMovie remoteMovie, string magnetUrl)
private string DownloadFromMagnetUrl(RemoteMovie remoteMovie, IIndexer indexer, string magnetUrl)
{
string hash = null;
string actualHash = null;
@@ -222,6 +230,8 @@ namespace NzbDrone.Core.Download
if (hash != null)
{
EnsureReleaseIsNotBlocklisted(remoteMovie, indexer, hash);
actualHash = AddFromMagnetLink(remoteMovie, hash, magnetUrl);
}
@@ -235,5 +245,30 @@ namespace NzbDrone.Core.Download
return actualHash;
}
private void EnsureReleaseIsNotBlocklisted(RemoteMovie remoteMovie, IIndexer indexer, string hash)
{
var indexerSettings = indexer?.Definition?.Settings as ITorrentIndexerSettings;
var torrentInfo = remoteMovie.Release as TorrentInfo;
var torrentInfoHash = torrentInfo?.InfoHash;
// If the release didn't come from an interactive search,
// the hash wasn't known during processing and the
// indexer is configured to reject blocklisted releases
// during grab check if it's already been blocklisted.
if (torrentInfo != null && torrentInfoHash.IsNullOrWhiteSpace())
{
// If the hash isn't known from parsing we set it here so it can be used for blocklisting.
torrentInfo.InfoHash = hash;
if (remoteMovie.ReleaseSource != ReleaseSourceType.InteractiveSearch &&
indexerSettings?.RejectBlocklistedTorrentHashesWhileGrabbing == true &&
_blocklistService.BlocklistedTorrentHash(remoteMovie.Movie.Id, hash))
{
throw new ReleaseBlockedException(remoteMovie.Release, "Release previously added to blocklist");
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More