1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -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
+2 -2
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.2.4' majorVersion: '5.3.1'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.413' dotnetVersion: '6.0.417'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css'; import styles from './HistoryEventTypeCell.css';
function getIconName(eventType) { function getIconName(eventType, data) {
switch (eventType) { switch (eventType) {
case 'grabbed': case 'grabbed':
return icons.DOWNLOADING; return icons.DOWNLOADING;
@@ -17,7 +17,7 @@ function getIconName(eventType) {
case 'downloadFailed': case 'downloadFailed':
return icons.DOWNLOADING; return icons.DOWNLOADING;
case 'movieFileDeleted': case 'movieFileDeleted':
return icons.DELETE; return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
case 'movieFileRenamed': case 'movieFileRenamed':
return icons.ORGANIZE; return icons.ORGANIZE;
case 'downloadIgnored': case 'downloadIgnored':
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
case 'downloadFailed': case 'downloadFailed':
return translate('MovieDownloadFailedTooltip'); return translate('MovieDownloadFailedTooltip');
case 'movieFileDeleted': case 'movieFileDeleted':
return translate('MovieFileDeletedTooltip'); return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
case 'movieFileRenamed': case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip'); return translate('MovieFileRenamedTooltip');
case 'downloadIgnored': case 'downloadIgnored':
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
} }
function HistoryEventTypeCell({ eventType, data }) { function HistoryEventTypeCell({ eventType, data }) {
const iconName = getIconName(eventType); const iconName = getIconName(eventType, data);
const iconKind = getIconKind(eventType); const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data); const tooltip = getTooltip(eventType, data);
@@ -85,8 +85,13 @@
margin-top: 20px; margin-top: 20px;
} }
.studio,
.genres {
margin-left: 5px;
}
.links { .links {
margin-left: 8px; margin-left: 5px;
pointer-events: all; pointer-events: all;
} }
@@ -5,6 +5,7 @@ interface CssExports {
'certification': string; 'certification': string;
'content': string; 'content': string;
'exclusionIcon': string; 'exclusionIcon': string;
'genres': string;
'icons': string; 'icons': string;
'links': string; 'links': string;
'overlay': string; 'overlay': string;
@@ -14,6 +15,7 @@ interface CssExports {
'runtime': string; 'runtime': string;
'searchResult': string; 'searchResult': string;
'statusContainer': string; 'statusContainer': string;
'studio': string;
'title': string; 'title': string;
'titleContainer': string; 'titleContainer': string;
'titleRow': string; 'titleRow': string;
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
@@ -61,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
titleSlug, titleSlug,
year, year,
studio, studio,
genres,
status, status,
overview, overview,
ratings, ratings,
@@ -76,6 +78,7 @@ class AddNewMovieSearchResult extends Component {
hasFile, hasFile,
isAvailable, isAvailable,
movieFile, movieFile,
queueItem,
runtime, runtime,
movieRuntimeFormat, movieRuntimeFormat,
certification certification
@@ -197,13 +200,46 @@ class AddNewMovieSearchResult extends Component {
/> />
</Label> </Label>
{
ratings.imdb ?
<Label size={sizes.LARGE}>
<ImdbRating
ratings={ratings}
iconSize={13}
/>
</Label> :
null
}
{ {
!!studio && !!studio &&
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
{studio} <Icon
name={icons.STUDIO}
size={13}
/>
<span className={styles.studio}>
{studio}
</span>
</Label> </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 <Tooltip
anchor={ anchor={
<Label <Label
@@ -215,15 +251,15 @@ class AddNewMovieSearchResult extends Component {
/> />
<span className={styles.links}> <span className={styles.links}>
Links {translate('Links')}
</span> </span>
</Label> </Label>
} }
tooltip={ tooltip={
<MovieDetailsLinks <MovieDetailsLinks
tmdbId={tmdbId} tmdbId={tmdbId}
youTubeTrailerId={youTubeTrailerId}
imdbId={imdbId} imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId}
/> />
} }
canFlip={true} canFlip={true}
@@ -237,6 +273,7 @@ class AddNewMovieSearchResult extends Component {
hasMovieFiles={hasFile} hasMovieFiles={hasFile}
monitored={monitored} monitored={monitored}
isAvailable={isAvailable} isAvailable={isAvailable}
queueItem={queueItem}
id={id} id={id}
useLabel={true} useLabel={true}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
@@ -273,6 +310,7 @@ AddNewMovieSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
studio: PropTypes.string, studio: PropTypes.string,
genres: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
overview: PropTypes.string, overview: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
@@ -283,15 +321,19 @@ AddNewMovieSearchResult.propTypes = {
isExclusionMovie: PropTypes.bool.isRequired, isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
id: PropTypes.number, id: PropTypes.number,
queueItems: PropTypes.arrayOf(PropTypes.object),
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired, hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired, isAvailable: PropTypes.bool.isRequired,
movieFile: PropTypes.object, movieFile: PropTypes.object,
queueItem: PropTypes.object,
colorImpairedMode: PropTypes.bool, colorImpairedMode: PropTypes.bool,
runtime: PropTypes.number.isRequired, runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired, movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string certification: PropTypes.string
}; };
AddNewMovieSearchResult.defaultProps = {
genres: []
};
export default AddNewMovieSearchResult; export default AddNewMovieSearchResult;
@@ -10,14 +10,18 @@ function createMapStateToProps() {
createExistingMovieSelector(), createExistingMovieSelector(),
createExclusionMovieSelector(), createExclusionMovieSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(state) => state.queue.details.items,
(state, { internalId }) => internalId, (state, { internalId }) => internalId,
(state) => state.settings.ui.item.movieRuntimeFormat, (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 { return {
existingMovieId: internalId, existingMovieId: internalId,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
queueItem,
movieRuntimeFormat movieRuntimeFormat
}; };
} }
@@ -32,7 +32,7 @@
.contentContainer { .contentContainer {
z-index: $popperZIndex; z-index: $popperZIndex;
margin-top: 4px; margin-top: 4px;
/* 400px container witdh with 8px padding on each side */ /* 400px container width with 8px padding on each side */
width: 384px; width: 384px;
} }
@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
className={styles.addErrorAlert} className={styles.addErrorAlert}
kind={kinds.DANGER} kind={kinds.DANGER}
> >
{translate('UnableToAddRootFolder')} {translate('AddRootFolderError')}
<ul> <ul>
{ {
+9
View File
@@ -44,7 +44,16 @@ export interface CustomFilter {
filers: PropertyFilter[]; filers: PropertyFilter[];
} }
export interface AppSectionState {
dimensions: {
isSmallScreen: boolean;
width: number;
height: number;
};
}
interface AppState { interface AppState {
app: AppSectionState;
calendar: CalendarAppState; calendar: CalendarAppState;
commands: CommandAppState; commands: CommandAppState;
history: HistoryAppState; history: HistoryAppState;
+1 -1
View File
@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
gotoCalendarToday gotoCalendarToday
} = this.props; } = this.props;
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
if (useCurrentPage) { if (useCurrentPage) {
fetchCalendar(); fetchCalendar();
+3 -1
View File
@@ -167,7 +167,7 @@ class SignalRConnector extends Component {
const resource = body.resource; const resource = body.resource;
const status = resource.status; 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. // completed, otherwise they spin until they timeout.
if (status === 'completed' || status === 'failed') { if (status === 'completed' || status === 'failed') {
@@ -187,6 +187,8 @@ class SignalRConnector extends Component {
repopulatePage('movieFileUpdated'); repopulatePage('movieFileUpdated');
} else if (body.action === 'deleted') { } else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id }); this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('movieFileDeleted');
} }
}; };
@@ -15,5 +15,5 @@
"start_url": "../../../../", "start_url": "../../../../",
"theme_color": "#3a3f51", "theme_color": "#3a3f51",
"background_color": "#3a3f51", "background_color": "#3a3f51",
"display": "standalone" "display": "minimal-ui"
} }
+2
View File
@@ -59,6 +59,7 @@ import {
faEye as fasEye, faEye as fasEye,
faFastBackward as fasFastBackward, faFastBackward as fasFastBackward,
faFastForward as fasFastForward, faFastForward as fasFastForward,
faFileCircleQuestion as fasFileCircleQuestion,
faFileExport as fasFileExport, faFileExport as fasFileExport,
faFileInvoice as farFileInvoice, faFileInvoice as farFileInvoice,
faFilm as fasFilm, faFilm as fasFilm,
@@ -159,6 +160,7 @@ export const EXPORT = fasFileExport;
export const EXTERNAL_LINK = fasExternalLinkAlt; export const EXTERNAL_LINK = fasExternalLinkAlt;
export const FATAL = fasTimesCircle; export const FATAL = fasTimesCircle;
export const FILE = farFile; export const FILE = farFile;
export const FILE_MISSING = fasFileCircleQuestion;
export const FILM = fasFilm; export const FILM = fasFilm;
export const FILTER = fasFilter; export const FILTER = fasFilter;
export const FLAG = fasFlag; export const FLAG = fasFlag;
@@ -22,6 +22,10 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
} }
.quality {
white-space: nowrap;
}
.languages { .languages {
width: 100px; width: 100px;
} }
@@ -266,7 +266,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.quality}> <TableRowCell className={styles.quality}>
<MovieQuality quality={quality} /> <MovieQuality quality={quality} showRevision={true} />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
@@ -5,6 +5,29 @@ import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters'; import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster'; 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() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.movieCredits.items, (state) => state.movieCredits.items,
@@ -17,8 +40,10 @@ function createMapStateToProps() {
return acc; return acc;
}, []); }, []);
const sortedCrew = crew.sort(crewSort);
return { return {
items: _.uniqBy(crew, 'personName') items: _.uniqBy(sortedCrew, 'personName')
}; };
} }
); );
@@ -9,3 +9,9 @@
.container { .container {
padding: 10px; padding: 10px;
} }
.sliderContainer {
--swiper-navigation-color: var(--white);
display: block;
}
@@ -4,6 +4,7 @@ interface CssExports {
'container': string; 'container': string;
'grid': string; 'grid': string;
'movie': string; 'movie': string;
'sliderContainer': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -14,24 +14,6 @@ import 'swiper/css/navigation';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding); const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); 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) { function calculateRowHeight(posterHeight, isSmallScreen) {
const titleHeight = 19; const titleHeight = 19;
const characterHeight = 19; const characterHeight = 19;
@@ -46,10 +28,6 @@ function calculateRowHeight(posterHeight, isSmallScreen) {
return heights.reduce((acc, height) => acc + height, 0); return heights.reduce((acc, height) => acc + height, 0);
} }
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class MovieCreditPosters extends Component { class MovieCreditPosters extends Component {
// //
@@ -66,39 +44,16 @@ class MovieCreditPosters extends Component {
posterHeight: 238, posterHeight: 238,
rowHeight: calculateRowHeight(238, props.isSmallScreen) 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
render() { render() {
const { const {
items, items,
itemComponent itemComponent,
isSmallScreen
} = this.props; } = this.props;
const { const {
@@ -113,14 +68,13 @@ class MovieCreditPosters extends Component {
<Swiper <Swiper
slidesPerView='auto' slidesPerView='auto'
spaceBetween={10} spaceBetween={10}
slidesPerGroup={3} slidesPerGroup={isSmallScreen ? 1 : 3}
navigation={true}
loop={false} loop={false}
loopFillGroupWithBlank={true} loopFillGroupWithBlank={true}
className="mySwiper" className="mySwiper"
modules={[Navigation]} modules={[Navigation]}
onInit={(swiper) => { onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init(); swiper.navigation.init();
swiper.navigation.update(); swiper.navigation.update();
}} }}
+3 -2
View File
@@ -320,8 +320,8 @@ class MovieDetails extends Component {
/> />
<PageToolbarButton <PageToolbarButton
label={translate('ManualImport')} label={translate('ManageFiles')}
iconName={icons.INTERACTIVE} iconName={icons.MOVIE_FILE}
onPress={this.onInteractiveImportPress} onPress={this.onInteractiveImportPress}
/> />
@@ -704,6 +704,7 @@ class MovieDetails extends Component {
<InteractiveImportModal <InteractiveImportModal
isOpen={isInteractiveImportModalOpen} isOpen={isInteractiveImportModalOpen}
movieId={id} movieId={id}
modalTitle={translate('ManageFiles')}
folder={path} folder={path}
allowMovieChange={false} allowMovieChange={false}
showFilterExistingFiles={true} showFilterExistingFiles={true}
@@ -33,14 +33,11 @@ const selectMovieFiles = createSelector(
const hasMovieFiles = !!items.length; const hasMovieFiles = !!items.length;
const sizeOnDisk = items.map((item) => item.size).reduce((prev, curr) => prev + curr, 0);
return { return {
isMovieFilesFetching: isFetching, isMovieFilesFetching: isFetching,
isMovieFilesPopulated: isPopulated, isMovieFilesPopulated: isPopulated,
movieFilesError: error, movieFilesError: error,
hasMovieFiles, hasMovieFiles
sizeOnDisk
}; };
} }
); );
@@ -104,8 +101,7 @@ function createMapStateToProps() {
isMovieFilesFetching, isMovieFilesFetching,
isMovieFilesPopulated, isMovieFilesPopulated,
movieFilesError, movieFilesError,
hasMovieFiles, hasMovieFiles
sizeOnDisk
} = movieFiles; } = movieFiles;
const { const {
@@ -161,7 +157,6 @@ function createMapStateToProps() {
movieCreditsError, movieCreditsError,
extraFilesError, extraFilesError,
hasMovieFiles, hasMovieFiles,
sizeOnDisk,
previousMovie, previousMovie,
nextMovie, nextMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
@@ -6,31 +6,43 @@ import MovieTitlesTableContent from './MovieTitlesTableContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { movieId }) => movieId,
(state) => state.movies, (state) => state.movies,
(movies) => { (movieId, movies) => {
return 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 { class MovieTitlesTableContentConnector extends Component {
// //
// Render // Render
render() { render() {
const movie = this.props.items.filter((obj) => { const {
return obj.id === this.props.movieId; alternateTitles,
}); ...otherProps
} = this.props;
return ( return (
<MovieTitlesTableContent <MovieTitlesTableContent
{...this.props} {...otherProps}
items={movie[0].alternateTitles} items={alternateTitles}
/> />
); );
} }
@@ -38,7 +50,11 @@ class MovieTitlesTableContentConnector extends Component {
MovieTitlesTableContentConnector.propTypes = { MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired, 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);
@@ -56,7 +56,6 @@ const columns = [
}, },
{ {
name: 'actions', name: 'actions',
label: () => translate('Actions'),
isVisible: true isVisible: true
} }
]; ];
+4 -13
View File
@@ -7,8 +7,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip'; import { icons, kinds } from 'Helpers/Props';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
@@ -103,20 +102,11 @@ class MovieHistoryRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell>
<MovieFormats <MovieFormats formats={customFormats} />
formats={customFormats}
/>
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
<Tooltip {formatCustomFormatScore(customFormatScore, customFormats.length)}
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
</TableRowCell> </TableRowCell>
<RelativeDateCellConnector <RelativeDateCellConnector
@@ -134,6 +124,7 @@ class MovieHistoryRow extends Component {
<IconButton <IconButton
title={translate('MarkAsFailed')} title={translate('MarkAsFailed')}
name={icons.REMOVE} name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress} onPress={this.onMarkAsFailedPress}
/> />
} }
@@ -1,11 +1,12 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext'; import { useSelect } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState'; import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
import { MOVIE_SEARCH } from 'Commands/commandNames'; import { MOVIE_SEARCH } from 'Commands/commandNames';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; 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 { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector'; import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
@@ -21,11 +22,12 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH)); const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
const { const {
items, items,
totalItems,
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState = }: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex')); useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const { isSelectMode, selectedFilterKey } = props; const { isSelectMode, selectedFilterKey } = props;
const [selectState] = useSelect(); const [selectState] = useSelect();
const { selectedState } = selectState; const { selectedState } = selectState;
@@ -50,6 +52,8 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
: translate('SearchAll'); : translate('SearchAll');
const onPress = useCallback(() => { const onPress = useCallback(() => {
setIsConfirmModalOpen(false);
dispatch( dispatch(
executeCommand({ executeCommand({
name: MOVIE_SEARCH, name: MOVIE_SEARCH,
@@ -58,14 +62,36 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
); );
}, [dispatch, moviesToSearch]); }, [dispatch, moviesToSearch]);
const onConfirmPress = useCallback(() => {
setIsConfirmModalOpen(true);
}, [setIsConfirmModalOpen]);
const onConfirmModalClose = useCallback(() => {
setIsConfirmModalOpen(false);
}, [setIsConfirmModalOpen]);
return ( return (
<PageToolbarButton <>
label={isSelectMode ? searchSelectLabel : searchIndexLabel} <PageToolbarButton
isSpinning={isSearching} label={isSelectMode ? searchSelectLabel : searchIndexLabel}
isDisabled={!totalItems} isSpinning={isSearching}
iconName={icons.SEARCH} isDisabled={!items.length}
onPress={onPress} 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}
/>
</>
); );
} }
@@ -256,13 +256,18 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
if (current) { if (current) {
const width = current.clientWidth; const width = current.clientWidth;
const padding = bodyPadding - 5; const padding = bodyPadding - 5;
const finalWidth = width - padding * 2;
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
return;
}
setSize({ setSize({
width: width - padding * 2, width: finalWidth,
height: window.innerHeight, height: window.innerHeight,
}); });
} }
}, [isSmallScreen, scrollerRef, bounds]); }, [isSmallScreen, size, scrollerRef, bounds]);
useEffect(() => { useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement; const currentScrollerRef = scrollerRef.current as HTMLElement;
+46 -10
View File
@@ -3,6 +3,7 @@ import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) { function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
const revision = quality.revision; const revision = quality.revision;
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
return title; 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) { function MovieQuality(props) {
const { const {
className, className,
@@ -35,7 +66,8 @@ function MovieQuality(props) {
quality, quality,
size, size,
isMonitored, isMonitored,
isCutoffNotMet isCutoffNotMet,
showRevision
} = props; } = props;
let kind = kinds.DEFAULT; let kind = kinds.DEFAULT;
@@ -50,13 +82,15 @@ function MovieQuality(props) {
} }
return ( return (
<Label <span>
className={className} <Label
kind={kind} className={className}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)} kind={kind}
> title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
{quality.quality.name} >
</Label> {quality.quality.name}
</Label>{revisionLabel(className, quality, showRevision)}
</span>
); );
} }
@@ -66,12 +100,14 @@ MovieQuality.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
size: PropTypes.number, size: PropTypes.number,
isMonitored: PropTypes.bool, isMonitored: PropTypes.bool,
isCutoffNotMet: PropTypes.bool isCutoffNotMet: PropTypes.bool,
showRevision: PropTypes.bool
}; };
MovieQuality.defaultProps = { MovieQuality.defaultProps = {
title: '', title: '',
isMonitored: true isMonitored: true,
showRevision: false
}; };
export default MovieQuality; export default MovieQuality;
-82
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;
-92
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;
+1 -1
View File
@@ -49,7 +49,7 @@ function RootFolders() {
if (!isFetching && !!error) { if (!isFetching && !!error) {
return ( return (
<Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert> <Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert>
); );
} }
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteDownloadClients, bulkDeleteDownloadClients,
bulkEditDownloadClients, bulkEditDownloadClients,
setManageDownloadClientsSort,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps { interface ManageDownloadClientsModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageDownloadClientsModalContent( function ManageDownloadClientsModalContent(
@@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
isSaving, isSaving,
error, error,
items, items,
sortKey,
sortDirection,
}: DownloadClientAppState = useSelector( }: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients') createClientSideCollectionSelector('settings.downloadClients')
); );
@@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
const selectedCount = selectedIds.length; const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageDownloadClientsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => { const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]); }, [setIsDeleteModalOpen]);
@@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{items.map((item) => { {items.map((item) => {
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteIndexers, bulkDeleteIndexers,
bulkEditIndexers, bulkEditIndexers,
setManageIndexersSort,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
@@ -80,6 +82,8 @@ const COLUMNS = [
interface ManageIndexersModalContentProps { interface ManageIndexersModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isSaving, isSaving,
error, error,
items, items,
sortKey,
sortDirection,
}: IndexerAppState = useSelector( }: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers') createClientSideCollectionSelector('settings.indexers')
); );
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const selectedCount = selectedIds.length; const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageIndexersSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => { const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]); }, [setIsDeleteModalOpen]);
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{items.map((item) => { {items.map((item) => {
@@ -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;
@@ -1,14 +1,18 @@
import React, { useCallback, useState } from 'react'; 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 FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props'; import { icons, kinds, sizes } from 'Helpers/Props';
import { addRootFolder } from 'Store/Actions/rootFolderActions'; import { addRootFolder } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AddRootFolder.css'; import styles from './AddRootFolder.css';
function AddRootFolder() { function AddRootFolder() {
const { isSaving, saveError } = useSelector(createRootFoldersSelector());
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] = const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
@@ -30,24 +34,42 @@ function AddRootFolder() {
}, [setIsAddNewRootFolderModalOpen]); }, [setIsAddNewRootFolderModalOpen]);
return ( return (
<div className={styles.addRootFolderButtonContainer}> <>
<Button {!isSaving && saveError ? (
kind={kinds.PRIMARY} <Alert kind={kinds.DANGER}>
size={sizes.LARGE} {translate('AddRootFolderError')}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<FileBrowserModal <ul>
isOpen={isAddNewRootFolderModalOpen} {Array.isArray(saveError.responseJSON) ? (
name="rootFolderPath" saveError.responseJSON.map((e, index) => {
value="" return <li key={index}>{e.errorMessage}</li>;
onChange={onNewRootFolderSelect} })
onModalClose={onAddRootFolderModalClose} ) : (
/> <li>{JSON.stringify(saveError.responseJSON)}</li>
</div> )}
</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>
</>
); );
} }
@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; 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 createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; 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 createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; 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 TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient'; export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients'; 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_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 // Action Creators
@@ -48,9 +50,9 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT); export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT); export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS); 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 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) => { export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return { return {
@@ -90,7 +92,9 @@ export default {
isTesting: false, isTesting: false,
isTestingAll: false, isTestingAll: false,
items: [], items: [],
pendingChanges: {} pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
}, },
// //
@@ -124,7 +128,10 @@ export default {
return selectedSchema; return selectedSchema;
}); });
} },
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
} }
}; };
@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; 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 createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; 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 createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; 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 TEST_INDEXER = 'settings/indexers/testIndexer';
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer'; export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers'; 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_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
// //
// Action Creators // Action Creators
@@ -53,9 +55,9 @@ export const deleteIndexer = createThunk(DELETE_INDEXER);
export const testIndexer = createThunk(TEST_INDEXER); export const testIndexer = createThunk(TEST_INDEXER);
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER); export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS); export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
export const bulkEditIndexers = createThunk(BULK_EDIT_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) => { export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
return { return {
@@ -95,7 +97,9 @@ export default {
isTesting: false, isTesting: false,
isTestingAll: false, isTestingAll: false,
items: [], items: [],
pendingChanges: {} pendingChanges: {},
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
}, },
// //
@@ -157,7 +161,10 @@ export default {
}; };
return updateSectionState(state, section, newState); return updateSectionState(state, section, newState);
} },
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
} }
}; };
@@ -130,7 +130,10 @@ export const actionHandlers = handleThunks({
promise.done((data) => { promise.done((data) => {
const updatedItem = _.cloneDeep(data); const updatedItem = _.cloneDeep(data);
updatedItem.internalId = updatedItem.id;
updatedItem.id = updatedItem.tmdbId; updatedItem.id = updatedItem.tmdbId;
delete updatedItem.images;
const actions = [ const actions = [
updateItem({ section: 'movies', ...data }), updateItem({ section: 'movies', ...data }),
updateItem({ section: 'addMovie', ...updatedItem }), updateItem({ section: 'addMovie', ...updatedItem }),
@@ -209,7 +209,7 @@ export const defaultState = {
{ {
name: 'tags', name: 'tags',
label: () => translate('Tags'), label: () => translate('Tags'),
isSortable: false, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
@@ -1,8 +1,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createDimensionsSelector() { function createDimensionsSelector() {
return createSelector( return createSelector(
(state) => state.app.dimensions, (state: AppState) => state.app.dimensions,
(dimensions) => { (dimensions) => {
return dimensions; return dimensions;
} }
+1 -1
View File
@@ -28,7 +28,7 @@
"@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0", "@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.21", "@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2", "@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2", "@sentry/integrations": "7.51.2",
"@types/node": "18.16.8", "@types/node": "18.16.8",
@@ -1,6 +1,9 @@
using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Localization;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using Radarr.Http.ClientSchema; using Radarr.Http.ClientSchema;
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
[TestFixture] [TestFixture]
public class SchemaBuilderFixture : TestBase 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] [Test]
public void should_return_field_for_every_property() public void should_return_field_for_every_property()
{ {
@@ -47,7 +47,7 @@ namespace NzbDrone.Common.Test.Http
// Use mirrors for tests that use two hosts // Use mirrors for tests that use two hosts
var candidates = new[] { "httpbin1.servarr.com" }; 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; _httpBinHost = mainHost;
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray(); _httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
@@ -67,7 +67,7 @@ namespace NzbDrone.Common.EnvironmentInfo
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Warn(ex, "Coudn't set app folder permission"); _logger.Warn(ex, "Couldn't set app folder permission");
} }
} }
@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
{ {
data = new XElement("base64", Convert.ToBase64String(bytes)); 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 else
{ {
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}"); throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
@@ -123,7 +123,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
_debounce = new SentryDebounce(); _debounce = new SentryDebounce();
// initialize to true and reconfigure later // 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 // before config file gets read will not be filtered
FilterEvents = true; FilterEvents = true;
SentryEnabled = true; SentryEnabled = true;
+1 -1
View File
@@ -260,7 +260,7 @@ namespace NzbDrone.Common.OAuth
} }
/// <summary> /// <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. /// This is also known as the signature base.
/// </summary> /// </summary>
/// <seealso href="http://oauth.net/core/1.0#rfc.section.9.1.3"/> /// <seealso href="http://oauth.net/core/1.0#rfc.section.9.1.3"/>
+3 -3
View File
@@ -4,17 +4,17 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.3" /> <PackageReference Include="NLog" Version="5.2.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.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="Sentry" Version="3.23.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.8" /> <PackageReference Include="System.Text.Json" Version="6.0.9" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
+1 -1
View File
@@ -215,7 +215,7 @@ namespace NzbDrone.Common
if (dacls.Contains(authenticatedUsersDacl)) if (dacls.Contains(authenticatedUsersDacl))
{ {
// Permssions already set // Permissions already set
return; return;
} }
@@ -4,6 +4,7 @@ using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.ImportListMovies; using NzbDrone.Core.ImportLists.ImportListMovies;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
@@ -42,8 +43,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{ {
GivenImportList(); GivenImportList();
var movieMetadata = Builder<MovieMetadata>.CreateNew().BuildNew();
Db.Insert(movieMetadata);
var status = Builder<ImportListMovie>.CreateNew() var status = Builder<ImportListMovie>.CreateNew()
.With(h => h.ListId = _importList.Id) .With(h => h.ListId = _importList.Id)
.With(b => b.MovieMetadataId = movieMetadata.Id)
.BuildNew(); .BuildNew();
Db.Insert(status); Db.Insert(status);
@@ -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);
}
}
@@ -1,8 +1,10 @@
using NLog; using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Test.IndexerTests namespace NzbDrone.Core.Test.IndexerTests
{ {
@@ -31,5 +33,10 @@ namespace NzbDrone.Core.Test.IndexerTests
{ {
return _parser; return _parser;
} }
public new IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
{
return base.CleanupReleases(releases);
}
} }
} }
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestCase(HdrFormat.Hdr10Plus, "HDR10Plus")] [TestCase(HdrFormat.Hdr10Plus, "HDR10Plus")]
[TestCase(HdrFormat.DolbyVision, "DV")] [TestCase(HdrFormat.DolbyVision, "DV")]
[TestCase(HdrFormat.DolbyVisionHdr10, "DV HDR10")] [TestCase(HdrFormat.DolbyVisionHdr10, "DV HDR10")]
[TestCase(HdrFormat.DolbyVisionHdr10Plus, "DV HDR10Plus")]
[TestCase(HdrFormat.DolbyVisionHlg, "DV HLG")] [TestCase(HdrFormat.DolbyVisionHlg, "DV HLG")]
[TestCase(HdrFormat.DolbyVisionSdr, "DV SDR")] [TestCase(HdrFormat.DolbyVisionSdr, "DV SDR")]
public void should_format_video_dynamic_range_type(HdrFormat format, string expectedVideoDynamicRangeType) public void should_format_video_dynamic_range_type(HdrFormat format, string expectedVideoDynamicRangeType)
@@ -116,6 +116,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)] [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)] [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 1, HdrFormat.DolbyVisionHdr10)] [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", 2, HdrFormat.DolbyVisionSdr)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 4, HdrFormat.DolbyVisionHlg)] [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) public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, int? doviConfigId, HdrFormat expected)
@@ -48,13 +48,14 @@ namespace NzbDrone.Core.Test.NotificationTests
Subject.Definition = _traktDefinition; 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 _downloadMessage.MovieFile.MediaInfo = new MediaInfoModel
{ {
AudioChannelPositions = audioChannels, AudioChannelPositions = audioChannels,
AudioFormat = audioFormat, AudioFormat = audioFormat,
ScanType = scanType ScanType = scanType,
VideoHdrFormat = hdrFormat
}; };
_downloadMessage.MovieFile.Quality.Quality = quality; _downloadMessage.MovieFile.Quality.Quality = quality;
@@ -72,7 +73,7 @@ namespace NzbDrone.Core.Test.NotificationTests
[Test] [Test]
public void should_add_collection_movie_if_valid_mediainfo() 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); Subject.OnDownload(_downloadMessage);
@@ -80,15 +81,16 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t => .Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" && t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "5.1" && t.Movies.First().AudioChannels == "5.1" &&
t.Movies.First().Resolution == "hd_1080p" && t.Movies.First().Resolution == "uhd_4k" &&
t.Movies.First().MediaType == "bluray"), t.Movies.First().MediaType == "bluray" &&
t.Movies.First().Hdr == "hdr10"),
It.IsAny<string>()), Times.Once()); It.IsAny<string>()), Times.Once());
} }
[Test] [Test]
public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie() 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); Subject.OnDownload(_downloadMessage);
@@ -96,8 +98,9 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t => .Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" && t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "2.0" && t.Movies.First().AudioChannels == "2.0" &&
t.Movies.First().Resolution == "hd_1080p" && t.Movies.First().Resolution == "uhd_4k" &&
t.Movies.First().MediaType == "bluray"), t.Movies.First().MediaType == "bluray" &&
t.Movies.First().Hdr == "hdr10"),
It.IsAny<string>()), Times.Once()); It.IsAny<string>()), Times.Once());
} }
} }
@@ -140,6 +140,8 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[TestCase("Movie.Title.1994.Russian.1080p.XviD-LOL")] [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) public void should_parse_language_russian(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); 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.Hebrew.1080p.XviD-LOL")]
[TestCase("Movie.Title.1994.1080p.BluRay.HebDubbed.Also.English.x264-P2P")]
public void should_parse_language_hebrew(string postTitle) public void should_parse_language_hebrew(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); 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.lv.WEBRip.XviD-LOL")]
[TestCase("Movie.Title.2022.LATVIAN.WEBRip.XviD-LOL")] [TestCase("Movie.Title.2022.LATVIAN.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) public void should_parse_language_latvian(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle); var result = Parser.Parser.ParseMovieTitle(postTitle);
@@ -429,5 +434,35 @@ namespace NzbDrone.Core.Test.ParserTests
var result = LanguageParser.ParseSubtitleLanguage(fileName); var result = LanguageParser.ParseSubtitleLanguage(fileName);
result.Should().Be(Language.Unknown); 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);
}
} }
} }
@@ -62,6 +62,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("G.I.Movie.Movie.2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "G.I. Movie Movie")] [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("www.Torrenting.org - Movie.2008.720p.X264-DIMENSION", "Movie")]
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]", "The French 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) public void should_parse_movie_title(string postTitle, string title)
{ {
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(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("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("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("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) public void should_parse_german_movie(string postTitle, string title, string edition, int year)
{ {
var movie = Parser.Parser.ParseMovieTitle(postTitle); 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.Italian.Movie.2025.720p.BluRay.X264-AMIABLE")]
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]")] [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) public void should_not_parse_wrong_language_in_title(string postTitle)
{ {
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true); var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -245,22 +251,6 @@ namespace NzbDrone.Core.Test.ParserTests
parsed.Languages.First().Should().Be(Language.Unknown); 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.KORSUB.WEBRip.x264.AAC2.0-RADARR", "KORSUB")]
[TestCase("Movie.Title.2016.1080p.KORSUBS.WEBRip.x264.AAC2.0-RADARR", "KORSUBS")] [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")] [TestCase("Movie Title 2017 HC 720p HDRiP DD5 1 x264-LEGi0N", "Generic Hardcoded Subs")]
@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
public string Hint { get; set; } 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 class FieldSelectOption
{ {
public int Value { get; set; } public int Value { get; set; }
@@ -84,4 +101,11 @@ namespace NzbDrone.Core.Annotations
ApiKey, ApiKey,
UserName UserName
} }
public enum TokenField
{
Label,
HelpText,
HelpTextWarning
}
} }
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
public interface IBlocklistService public interface IBlocklistService
{ {
bool Blocklisted(int movieId, ReleaseInfo release); bool Blocklisted(int movieId, ReleaseInfo release);
bool BlocklistedTorrentHash(int movieId, string hash);
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec); PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
List<Blocklist> GetByMovieId(int movieId); List<Blocklist> GetByMovieId(int movieId);
void Block(RemoteMovie remoteMovie, string message); void Block(RemoteMovie remoteMovie, string message);
@@ -37,30 +38,34 @@ namespace NzbDrone.Core.Blocklisting
public bool Blocklisted(int movieId, ReleaseInfo release) public bool Blocklisted(int movieId, ReleaseInfo release)
{ {
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(movieId, release.Title);
if (release.DownloadProtocol == DownloadProtocol.Torrent) if (release.DownloadProtocol == DownloadProtocol.Torrent)
{ {
var torrentInfo = release as TorrentInfo; if (release is not TorrentInfo torrentInfo)
if (torrentInfo == null)
{ {
return false; return false;
} }
if (torrentInfo.InfoHash.IsNullOrWhiteSpace()) if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
{ {
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent) var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash);
.Any(b => SameTorrent(b, torrentInfo));
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
} }
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash); return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
.Where(b => b.Protocol == DownloadProtocol.Torrent)
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo)); .Any(b => SameTorrent(b, torrentInfo));
} }
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet) return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
.Any(b => SameNzb(b, release)); .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) public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
@@ -330,8 +330,8 @@ namespace NzbDrone.Core.Configuration
return; return;
} }
// If SSL is enabled and a cert hash is still in the config file disable SSL // 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()) if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
{ {
SetValue("EnableSsl", false); SetValue("EnableSsl", false);
} }
@@ -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));
}
}
}
@@ -9,8 +9,8 @@ namespace NzbDrone.Core.Datastore
{ {
public interface IConnectionStringFactory public interface IConnectionStringFactory
{ {
string MainDbConnectionString { get; } DatabaseConnectionInfo MainDbConnection { get; }
string LogDbConnectionString { get; } DatabaseConnectionInfo LogDbConnection { get; }
string GetDatabasePath(string connectionString); string GetDatabasePath(string connectionString);
} }
@@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore
{ {
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) : MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
GetConnectionString(appFolderInfo.GetDatabase()); GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) : LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
GetConnectionString(appFolderInfo.GetLogDatabase()); GetConnectionString(appFolderInfo.GetLogDatabase());
} }
public string MainDbConnectionString { get; private set; } public DatabaseConnectionInfo MainDbConnection { get; private set; }
public string LogDbConnectionString { get; private set; } public DatabaseConnectionInfo LogDbConnection { get; private set; }
public string GetDatabasePath(string connectionString) public string GetDatabasePath(string connectionString)
{ {
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.DataSource; return connectionBuilder.DataSource;
} }
private static string GetConnectionString(string dbPath) private static DatabaseConnectionInfo GetConnectionString(string dbPath)
{ {
var connectionBuilder = new SQLiteConnectionStringBuilder var connectionBuilder = new SQLiteConnectionStringBuilder
{ {
@@ -57,21 +57,22 @@ namespace NzbDrone.Core.Datastore
connectionBuilder.Add("Full FSync", true); 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; return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
connectionBuilder.Host = _configFileProvider.PostgresHost;
connectionBuilder.Username = _configFileProvider.PostgresUser;
connectionBuilder.Password = _configFileProvider.PostgresPassword;
connectionBuilder.Port = _configFileProvider.PostgresPort;
connectionBuilder.Enlist = false;
return connectionBuilder.ConnectionString;
} }
} }
} }
@@ -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; }
}
}
+18 -15
View File
@@ -2,6 +2,7 @@ using System;
using System.Data.Common; using System.Data.Common;
using System.Data.SQLite; using System.Data.SQLite;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading;
using NLog; using NLog;
using Npgsql; using Npgsql;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -60,22 +61,22 @@ namespace NzbDrone.Core.Datastore
public IDatabase Create(MigrationContext migrationContext) public IDatabase Create(MigrationContext migrationContext)
{ {
string connectionString; DatabaseConnectionInfo connectionInfo;
switch (migrationContext.MigrationType) switch (migrationContext.MigrationType)
{ {
case MigrationType.Main: case MigrationType.Main:
{ {
connectionString = _connectionStringFactory.MainDbConnectionString; connectionInfo = _connectionStringFactory.MainDbConnection;
CreateMain(connectionString, migrationContext); CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break; break;
} }
case MigrationType.Log: case MigrationType.Log:
{ {
connectionString = _connectionStringFactory.LogDbConnectionString; connectionInfo = _connectionStringFactory.LogDbConnection;
CreateLog(connectionString, migrationContext); CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
break; break;
} }
@@ -90,14 +91,14 @@ namespace NzbDrone.Core.Datastore
{ {
DbConnection conn; DbConnection conn;
if (connectionString.Contains(".db")) if (connectionInfo.DatabaseType == DatabaseType.SQLite)
{ {
conn = SQLiteFactory.Instance.CreateConnection(); conn = SQLiteFactory.Instance.CreateConnection();
conn.ConnectionString = connectionString; conn.ConnectionString = connectionInfo.ConnectionString;
} }
else else
{ {
conn = new NpgsqlConnection(connectionString); conn = new NpgsqlConnection(connectionInfo.ConnectionString);
} }
conn.Open(); conn.Open();
@@ -107,12 +108,12 @@ namespace NzbDrone.Core.Datastore
return db; return db;
} }
private void CreateMain(string connectionString, MigrationContext migrationContext) private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{ {
try try
{ {
_restoreDatabaseService.Restore(); _restoreDatabaseService.Restore();
_migrationController.Migrate(connectionString, migrationContext); _migrationController.Migrate(connectionString, migrationContext, databaseType);
} }
catch (SQLiteException e) catch (SQLiteException e)
{ {
@@ -135,15 +136,17 @@ namespace NzbDrone.Core.Datastore
{ {
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount); Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
Thread.Sleep(5000);
try try
{ {
_migrationController.Migrate(connectionString, migrationContext); _migrationController.Migrate(connectionString, migrationContext, databaseType);
return;
} }
catch (Exception ex) catch (Exception ex)
{ {
if (--retryCount > 0) if (--retryCount > 0)
{ {
System.Threading.Thread.Sleep(5000);
continue; 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 try
{ {
_migrationController.Migrate(connectionString, migrationContext); _migrationController.Migrate(connectionString, migrationContext, databaseType);
} }
catch (SQLiteException e) 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."); 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) catch (Exception e)
{ {
@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Datastore
} }
/// <summary> /// <summary>
/// Visits the memeber access expression. To be implemented by user. /// Visits the member access expression. To be implemented by user.
/// </summary> /// </summary>
/// <param name="expression"></param> /// <param name="expression"></param>
/// <returns></returns> /// <returns></returns>
@@ -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();
}
}
}
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{ {
public interface IMigrationController public interface IMigrationController
{ {
void Migrate(string connectionString, MigrationContext migrationContext); void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType);
} }
public class MigrationController : IMigrationController public class MigrationController : IMigrationController
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_migrationLoggerProvider = migrationLoggerProvider; _migrationLoggerProvider = migrationLoggerProvider;
} }
public void Migrate(string connectionString, MigrationContext migrationContext) public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
ServiceProvider serviceProvider; ServiceProvider serviceProvider;
var db = connectionString.Contains(".db") ? "sqlite" : "postgres"; var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
serviceProvider = new ServiceCollection() serviceProvider = new ServiceCollection()
.AddLogging(b => b.AddNLog()) .AddLogging(b => b.AddNLog())
@@ -108,7 +108,7 @@ namespace NzbDrone.Core.DecisionEngine
private int ComparePeersIfTorrent(DownloadDecision x, DownloadDecision y) private int ComparePeersIfTorrent(DownloadDecision x, DownloadDecision y)
{ {
// Different protocols should get caught when checking the preferred protocol, // 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 || if (x.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent ||
y.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent) y.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent)
{ {
@@ -78,7 +78,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return false; 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; return true;
} }
@@ -7,9 +7,9 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -26,11 +26,11 @@ namespace NzbDrone.Core.Download.Clients.Aria2
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using System.Xml.XPath; using System.Xml.XPath;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions; using NzbDrone.Core.Download.Extensions;
@@ -97,8 +98,14 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public string AddMagnet(Aria2Settings settings, string magnet) 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(); var gid = response.GetStringResponse();
return gid; return gid;
@@ -106,8 +113,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public string AddTorrent(Aria2Settings settings, byte[] torrent) 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(); var gid = response.GetStringResponse();
return gid; return gid;
@@ -40,6 +40,10 @@ namespace NzbDrone.Core.Download.Clients.Aria2
[FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] [FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string SecretToken { get; set; } public string SecretToken { get; set; }
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")]
public string Directory { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
@@ -23,15 +23,13 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
private readonly Logger _logger; private readonly Logger _logger;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService; private readonly IDiskScanService _diskScanService;
private readonly INamingConfigService _namingConfigService;
private readonly ICached<Dictionary<string, WatchFolderItem>> _watchFolderItemCache; 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; _logger = logger;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_diskScanService = diskScanService; _diskScanService = diskScanService;
_namingConfigService = namingConfigService;
_watchFolderItemCache = cacheManager.GetCache<Dictionary<string, WatchFolderItem>>(GetType()); _watchFolderItemCache = cacheManager.GetCache<Dictionary<string, WatchFolderItem>>(GetType());
} }
@@ -7,6 +7,7 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
@@ -27,11 +28,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_scanWatchFolder = scanWatchFolder; _scanWatchFolder = scanWatchFolder;
@@ -22,12 +22,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
public UsenetBlackhole(IScanWatchFolder scanWatchFolder, public UsenetBlackhole(IScanWatchFolder scanWatchFolder,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService, IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger) : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{ {
_scanWatchFolder = scanWatchFolder; _scanWatchFolder = scanWatchFolder;
@@ -7,9 +7,9 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -24,11 +24,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -8,10 +8,10 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@@ -35,11 +35,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_dsInfoProxy = dsInfoProxy; _dsInfoProxy = dsInfoProxy;
_dsTaskProxySelector = dsTaskProxySelector; _dsTaskProxySelector = dsTaskProxySelector;
@@ -9,7 +9,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@@ -32,12 +31,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
IDownloadStationTaskProxySelector dsTaskProxySelector, IDownloadStationTaskProxySelector dsTaskProxySelector,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService, IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger) : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{ {
_dsInfoProxy = dsInfoProxy; _dsInfoProxy = dsInfoProxy;
_dsTaskProxySelector = dsTaskProxySelector; _dsTaskProxySelector = dsTaskProxySelector;
@@ -6,10 +6,10 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Flood.Models; using NzbDrone.Core.Download.Clients.Flood.Models;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@@ -26,11 +26,11 @@ namespace NzbDrone.Core.Download.Clients.Flood
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxy = proxy; _proxy = proxy;
_downloadSeedConfigProvider = downloadSeedConfigProvider; _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"; public override string Name => "Flood";
@@ -3,14 +3,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses; using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
@@ -21,15 +20,14 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
private readonly IFreeboxDownloadProxy _proxy; private readonly IFreeboxDownloadProxy _proxy;
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy, public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService, IDiskProvider diskProvider,
IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService,
IRemotePathMappingService remotePathMappingService, IBlocklistService blocklistService,
ICacheManager cacheManager, Logger logger)
Logger logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -6,10 +6,10 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Hadouken.Models; using NzbDrone.Core.Download.Clients.Hadouken.Models;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -24,11 +24,11 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -8,7 +8,6 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -22,12 +21,11 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
public NzbVortex(INzbVortexProxy proxy, public NzbVortex(INzbVortexProxy proxy,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService, IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger) : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -10,7 +10,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -26,12 +25,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
public Nzbget(INzbgetProxy proxy, public Nzbget(INzbgetProxy proxy,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService, IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger) : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -21,11 +21,10 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
public Pneumatic(IHttpClient httpClient, public Pneumatic(IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
Logger logger) Logger logger)
: base(configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(configService, diskProvider, remotePathMappingService, logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
} }
@@ -8,9 +8,9 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -32,12 +32,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
ICacheManager cacheManager, ICacheManager cacheManager,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxySelector = proxySelector; _proxySelector = proxySelector;
@@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentContentLayout
{
Default = 0,
Original = 1,
Subfolder = 2
}
}
@@ -265,6 +265,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
request.AddFormParameter("firstLastPiecePrio", true); 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) public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
@@ -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+)")] [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; } 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() public NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
@@ -10,7 +10,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -24,12 +23,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public Sabnzbd(ISabnzbdProxy proxy, public Sabnzbd(ISabnzbdProxy proxy,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IValidateNzbs nzbValidationService, IValidateNzbs nzbValidationService,
Logger logger) Logger logger)
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger) : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -4,9 +4,9 @@ using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Transmission namespace NzbDrone.Core.Download.Clients.Transmission
@@ -17,11 +17,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
} }
@@ -6,9 +6,9 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -23,11 +23,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxy = proxy; _proxy = proxy;
} }
@@ -141,7 +141,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private TransmissionResponse GetSessionVariables(TransmissionSettings settings) 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); return ProcessRequest("session-get", null, settings);
} }
@@ -2,10 +2,10 @@ using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Transmission; using NzbDrone.Core.Download.Clients.Transmission;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Vuze namespace NzbDrone.Core.Download.Clients.Vuze
@@ -18,11 +18,11 @@ namespace NzbDrone.Core.Download.Clients.Vuze
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(proxy, torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
} }
@@ -8,11 +8,11 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.rTorrent; using NzbDrone.Core.Download.Clients.rTorrent;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@@ -31,13 +31,13 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IDownloadSeedConfigProvider downloadSeedConfigProvider, IDownloadSeedConfigProvider downloadSeedConfigProvider,
IRTorrentDirectoryValidator rTorrentDirectoryValidator, IRTorrentDirectoryValidator rTorrentDirectoryValidator,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxy = proxy; _proxy = proxy;
_rTorrentDirectoryValidator = rTorrentDirectoryValidator; _rTorrentDirectoryValidator = rTorrentDirectoryValidator;
@@ -8,9 +8,9 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -27,11 +27,11 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
ITorrentFileInfoReader torrentFileInfoReader, ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
{ {
_proxy = proxy; _proxy = proxy;
@@ -1,16 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using Polly;
using Polly.Retry;
namespace NzbDrone.Core.Download namespace NzbDrone.Core.Download
{ {
@@ -18,11 +21,41 @@ namespace NzbDrone.Core.Download
where TSettings : IProviderConfig, new() where TSettings : IProviderConfig, new()
{ {
protected readonly IConfigService _configService; protected readonly IConfigService _configService;
protected readonly INamingConfigService _namingConfigService;
protected readonly IDiskProvider _diskProvider; protected readonly IDiskProvider _diskProvider;
protected readonly IRemotePathMappingService _remotePathMappingService; protected readonly IRemotePathMappingService _remotePathMappingService;
protected readonly Logger _logger; 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 abstract string Name { get; }
public Type ConfigContract => typeof(TSettings); public Type ConfigContract => typeof(TSettings);
@@ -41,13 +74,11 @@ namespace NzbDrone.Core.Download
protected TSettings Settings => (TSettings)Definition.Settings; protected TSettings Settings => (TSettings)Definition.Settings;
protected DownloadClientBase(IConfigService configService, protected DownloadClientBase(IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
Logger logger) Logger logger)
{ {
_configService = configService; _configService = configService;
_namingConfigService = namingConfigService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_remotePathMappingService = remotePathMappingService; _remotePathMappingService = remotePathMappingService;
_logger = logger; _logger = logger;
@@ -58,10 +89,7 @@ namespace NzbDrone.Core.Download
return GetType().Name; return GetType().Name;
} }
public abstract DownloadProtocol Protocol public abstract DownloadProtocol Protocol { get; }
{
get;
}
public abstract Task<string> Download(RemoteMovie remoteMovie, IIndexer indexer); public abstract Task<string> Download(RemoteMovie remoteMovie, IIndexer indexer);
public abstract IEnumerable<DownloadClientItem> GetItems(); public abstract IEnumerable<DownloadClientItem> GetItems();
@@ -103,6 +103,11 @@ namespace NzbDrone.Core.Download
_logger.Trace("Release {0} no longer available on indexer.", remoteMovie); _logger.Trace("Release {0} no longer available on indexer.", remoteMovie);
throw; throw;
} }
catch (ReleaseBlockedException)
{
_logger.Trace("Release {0} previously added to blocklist, not sending to download client again.", remoteMovie);
throw;
}
catch (DownloadClientRejectedReleaseException) catch (DownloadClientRejectedReleaseException)
{ {
_logger.Trace("Release {0} rejected by download client, possible duplicate.", remoteMovie); _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.DownloadClientId = downloadClient.Definition.Id;
movieGrabbedEvent.DownloadClientName = downloadClient.Definition.Name; movieGrabbedEvent.DownloadClientName = downloadClient.Definition.Name;
if (!string.IsNullOrWhiteSpace(downloadClientId)) if (downloadClientId.IsNotNullOrWhiteSpace())
{ {
movieGrabbedEvent.DownloadId = downloadClientId; movieGrabbedEvent.DownloadId = downloadClientId;
} }
@@ -129,7 +129,7 @@ namespace NzbDrone.Core.Download
private void PublishDownloadFailedEvent(List<MovieHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false) 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); Enum.TryParse(historyItem.Data.GetValueOrDefault(MovieHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
var downloadFailedEvent = new DownloadFailedEvent var downloadFailedEvent = new DownloadFailedEvent
@@ -326,7 +326,8 @@ namespace NzbDrone.Core.Download.Pending
Reason = reason, Reason = reason,
AdditionalInfo = new PendingReleaseAdditionalInfo AdditionalInfo = new PendingReleaseAdditionalInfo
{ {
MovieMatchType = decision.RemoteMovie.MovieMatchType MovieMatchType = decision.RemoteMovie.MovieMatchType,
ReleaseSource = decision.RemoteMovie.ReleaseSource
} }
}; };
@@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@@ -21,18 +22,20 @@ namespace NzbDrone.Core.Download
where TSettings : IProviderConfig, new() where TSettings : IProviderConfig, new()
{ {
protected readonly IHttpClient _httpClient; protected readonly IHttpClient _httpClient;
private readonly IBlocklistService _blocklistService;
protected readonly ITorrentFileInfoReader _torrentFileInfoReader; protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader, protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient, IHttpClient httpClient,
IConfigService configService, IConfigService configService,
INamingConfigService namingConfigService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService, IRemotePathMappingService remotePathMappingService,
IBlocklistService blocklistService,
Logger logger) Logger logger)
: base(configService, namingConfigService, diskProvider, remotePathMappingService, logger) : base(configService, diskProvider, remotePathMappingService, logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_blocklistService = blocklistService;
_torrentFileInfoReader = torrentFileInfoReader; _torrentFileInfoReader = torrentFileInfoReader;
} }
@@ -87,7 +90,7 @@ namespace NzbDrone.Core.Download
{ {
try try
{ {
return DownloadFromMagnetUrl(remoteMovie, magnetUrl); return DownloadFromMagnetUrl(remoteMovie, indexer, magnetUrl);
} }
catch (NotSupportedException ex) catch (NotSupportedException ex)
{ {
@@ -101,7 +104,7 @@ namespace NzbDrone.Core.Download
{ {
try try
{ {
return DownloadFromMagnetUrl(remoteMovie, magnetUrl); return DownloadFromMagnetUrl(remoteMovie, indexer, magnetUrl);
} }
catch (NotSupportedException ex) catch (NotSupportedException ex)
{ {
@@ -134,7 +137,9 @@ namespace NzbDrone.Core.Download
request.Headers.Accept = "application/x-bittorrent"; request.Headers.Accept = "application/x-bittorrent";
request.AllowAutoRedirect = false; 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 || if (response.StatusCode == HttpStatusCode.MovedPermanently ||
response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.Found ||
@@ -148,7 +153,7 @@ namespace NzbDrone.Core.Download
{ {
if (locationHeader.StartsWith("magnet:")) if (locationHeader.StartsWith("magnet:"))
{ {
return DownloadFromMagnetUrl(remoteMovie, locationHeader); return DownloadFromMagnetUrl(remoteMovie, indexer, locationHeader);
} }
request.Url += new HttpUri(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 filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteMovie.Release.Title));
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile); var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
EnsureReleaseIsNotBlocklisted(remoteMovie, indexer, hash);
var actualHash = AddFromTorrentFile(remoteMovie, hash, filename, torrentFile); var actualHash = AddFromTorrentFile(remoteMovie, hash, filename, torrentFile);
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
@@ -204,7 +212,7 @@ namespace NzbDrone.Core.Download
return actualHash; return actualHash;
} }
private string DownloadFromMagnetUrl(RemoteMovie remoteMovie, string magnetUrl) private string DownloadFromMagnetUrl(RemoteMovie remoteMovie, IIndexer indexer, string magnetUrl)
{ {
string hash = null; string hash = null;
string actualHash = null; string actualHash = null;
@@ -222,6 +230,8 @@ namespace NzbDrone.Core.Download
if (hash != null) if (hash != null)
{ {
EnsureReleaseIsNotBlocklisted(remoteMovie, indexer, hash);
actualHash = AddFromMagnetLink(remoteMovie, hash, magnetUrl); actualHash = AddFromMagnetLink(remoteMovie, hash, magnetUrl);
} }
@@ -235,5 +245,30 @@ namespace NzbDrone.Core.Download
return actualHash; 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