1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-26 17:44:24 -04:00

Compare commits

...

76 Commits

Author SHA1 Message Date
Bogdan
c96b3c4b0b Fixed: Autotagging Genres are case insensitive
(cherry picked from commit 87ecbf39c1c0cc8a3a3f4ee1d1878b34ea49f6b8)

Closes #9436
2023-11-20 00:59:06 +02:00
Mark McDowall
78bc9f9b4b Fixed: Saving indexer, download client, etc settings
(cherry picked from commit 804a5921b3b620e2407d5d6a7fd69fb1fd9b0cbf)
2023-11-20 00:53:45 +02:00
Servarr
b737f05a83 Automated API Docs update 2023-11-19 22:40:17 +02:00
Bogdan
8d96fd2387 More cleanup appName tokens 2023-11-19 22:35:57 +02:00
servarr[bot]
6c487ead00 Fixed: Disable SSL when using the removed SslCertHash configuration (#9426)
(cherry picked from commit d95660d3c78d1ee11a7966d58e78a82a8df01393)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2023-11-19 21:46:06 +02:00
Bogdan
22e3cf844c New: Confirmation before clearing blocklist
(cherry picked from commit 366d8a4e78e024bc045e4791d3a51af391f4e95a)

Closes #9429
Fixes #9401
2023-11-19 21:17:00 +02:00
Bogdan
14b4b5e122 Cleanup appName tokens
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>

Closes #9430
2023-11-19 21:14:48 +02:00
Mark McDowall
10f5f3c5c8 New: Require password confirmation when setting or changing password
(cherry picked from commit b248163df598dc611ee919d525eb7357256d73d5)
2023-11-19 21:10:52 +02:00
Bogdan
c687b552f0 Bump version to 5.2.2 2023-11-19 07:06:29 +02:00
Stevie Robinson
92c8c8a7f5 Translate System pages
(cherry picked from commit 93e8ff0ac7610fa8739f2e577ece98c2c06c8881)

Closes #8852
2023-11-18 23:44:43 +02:00
Stevie Robinson
86a16c3c0c Fix translation token for Update Check
(cherry picked from commit cd1d8a3ff04b0d06f6fa7dfeb7a7ab6dd88b36b3)

Closes #9341
2023-11-18 23:44:41 +02:00
Weblate
e8c280db34 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Francisco Cachado <franciscomcachado@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bowsefather <husseinali39@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
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/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2023-11-18 03:21:28 +02:00
Nesego
ea65e2174c Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (1336 of 1336 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
2023-11-18 03:01:28 +02:00
Weblate
4aa2466693 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Francisco Cachado <franciscomcachado@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bowsefather <husseinali39@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
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/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2023-11-18 02:59:39 +02:00
Bogdan
4df0f0f721 Revert translation keys 2023-11-18 02:49:58 +02:00
Mark McDowall
d7bee375e8 Use named tokens for backend translations
(cherry picked from commit 11f96c31048c2d1aafca0c91736d439f7f9a95a8)
2023-11-18 02:37:33 +02:00
Bogdan
906295466d Use variable for App name in translations
Co-authored-by: Qstick <qstick@gmail.com>
2023-11-18 02:37:33 +02:00
Mark McDowall
f86060eca2 Don't retest unchanged providers
New: Don't retest connections, indexers, download clients, etc if re-saved with the exact same settings

Closes #9411
Fixes ##9397

(cherry picked from commit 71fd09f162b2880c461e03cba4317c34ee3203dc)
2023-11-17 16:27:42 +02:00
Mark McDowall
bf9a0b62f2 Rename 'ReturnUrl' to 'returnUrl' for forms auth redirection
(cherry picked from commit 812712e2843a738054c065a6d5c1b7c81c5f8e7b)
2023-11-17 03:30:03 +02:00
Bogdan
ccc62f0450 Fixed: Enforce validation warnings when testing providers
(cherry picked from commit c3b4126d0c4f449a41e2cf7ea438b20e25370995)
2023-11-17 02:40:39 +02:00
bakerboy448
524657ad78 Improve sample detection logging (#9405)
Sonarr 6181
2023-11-15 19:38:58 +02:00
Qstick
7a394ff864 Update menu background on dark theme 2023-11-14 20:57:26 -06:00
Bogdan
d8862eedd3 Fixed: Refresh discovery movies state on refresh lists 2023-11-14 20:36:12 +02:00
Bogdan
71f700e240 Fixed: (HDBits) Add labels for categories, codecs and mediums 2023-11-14 17:13:34 +02:00
Bogdan
ae5dd84e0a Fixed: (HDBits) Increase search limit to 100 releases 2023-11-14 15:13:45 +02:00
Bogdan
17b398cf62 Bump version to 5.2.1 2023-11-12 16:52:53 +02:00
Qstick
d00678c1ba Bump ffprobe to 5.1.4
(cherry picked from commit e68b13940d4dd4c5fd008884d909e614521c8cee)
2023-11-11 20:04:59 +02:00
Bogdan
03df9b7f07 Add troubleshooting link when movie searches fail 2023-11-11 18:45:34 +02:00
Bogdan
3442a0ecca Bump version to 5.2.0 2023-11-10 06:24:33 +02:00
Bogdan
3376a467ca Fixed: Record status for notifications on tests 2023-11-10 05:07:29 +02:00
Servarr
1650ce17fb Automated API Docs update 2023-11-10 03:33:52 +02:00
Bogdan
2f2004faa2 New: Resolve download client by name using 'downloadClient' for pushed releases
(cherry picked from commit 0496e728dc1e21b16b9ee2fee010de9707bdff85)
2023-11-10 03:14:38 +02:00
Mark McDowall
437e2f4597 Don't store successful results for invalid providers
(cherry picked from commit de23182d593e2284972103d505e66dd8d812dfdb)
2023-11-10 03:13:27 +02:00
Bogdan
17b8605751 Simply queue item for movie details page 2023-11-09 20:53:49 +02:00
Bogdan
b2a52e52b6 Fixed: (UI) Progress bar status for searching existing movies 2023-11-09 20:53:49 +02:00
Servarr
0f5fabdfcd Automated API Docs update 2023-11-09 15:11:53 +02:00
Bogdan
6362ee9b7d Fixed: (UI) Import Lists enabled status doesn't rely on automatic add
Fixes #9375
2023-11-09 14:27:10 +02:00
Bogdan
50465fd482 New: Add enabled to manage import lists bulk 2023-11-09 14:16:03 +02:00
Bogdan
54d447d55f New: Parse VIE as Vietnamese language 2023-11-08 20:37:07 +02:00
Bogdan
50f48277e5 Don't show already imported movies as downloading in Movies index 2023-11-08 17:49:29 +02:00
Weblate
c2e206b7ac Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
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/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-11-07 20:29:36 +02:00
Bogdan
7a46de602f Increase the wait timeout for integration tests init 2023-11-06 00:29:02 +02:00
bakerboy448
89820c1ff7 Lower log level for missing translation/culture resource (#8956)
Fixes #8150
2023-11-04 16:21:47 +02:00
Stevie Robinson
67e6e129ff Fix event ids for history custom filters 2023-11-02 00:37:54 +02:00
Bogdan
b6001238e5 Sort root folders by path in UI
Fixes #9280
2023-11-01 13:13:21 +02:00
Stevie Robinson
7bbdcc81bb Use Diacritical.Net library for TitleFirstCharacter token
(cherry picked from commit 59ea524e0ce85333779f430b867e93aae366289f)

Closes #9324
2023-11-01 13:13:21 +02:00
Stevie Robinson
3e8cbc497e Fixed: Cleanup First Character in Title when using 'TitleFirstCharacter'
(cherry picked from commit b3c691859a0841cde9bb2655c01c240cc5279d74)

Closes #9249
2023-11-01 13:13:21 +02:00
Bogdan
60d2df043b Rename instances of Profile to QualityProfile
Closes #9337
2023-11-01 13:13:21 +02:00
Mark McDowall
da41cb8840 Fixed: Blocking unknown indexers from pushed releases
(cherry picked from commit 44d8dbaac81706691124ae5f8317289f0a3e5d73)
2023-10-30 14:21:29 +02:00
Bogdan
a4b7c99d91 Fix full disk releases regex for DVDs
(cherry picked from commit aa8659eecd0a0fedd56eb1066730d1d1ec32df0d)

Closes #9346
2023-10-29 18:50:38 +02:00
Bogdan
8fb21e073b Bump version to 5.1.3 2023-10-29 10:35:11 +02:00
Stevie Robinson
dbf424d454 New: Relative path as default Sort order on Manual Import
(cherry picked from commit 6e2162ebf4b3366df540bd35ad91bd5527976728)

Closes #9344
2023-10-29 01:12:45 +03:00
Bogdan
a6dda70c0a New: Add Download Client validation for indexers
(cherry picked from commit e53b7f8c945e3597ca1719961e82540f1f01f0e9)

Closes #9338
2023-10-29 01:05:50 +03:00
Bogdan
e6fa14b1e6 Allow 0 as valid value in QualityProfileExistsValidator
(cherry picked from commit 36ca24e55a5eda859047d82855f65c401cc0b30f)
2023-10-29 00:59:16 +03:00
Bogdan
b5c0d515ee Improve UI notice for delayed queue items
(cherry picked from commit f2cae4e2b2f87924973a00431be7216296648dac)
2023-10-29 00:59:02 +03:00
Bogdan
b7aee25d0d Set proper default props for Queue details and status
(cherry picked from commit dc099a77ca9e6993d386a2b70f1aff1fed34a32b)
2023-10-29 00:58:30 +03:00
Bogdan
233b85aaf3 Sort Custom Formats by name
(cherry picked from commit e9bb1d52a72b20a58d1a672ecfa3797eda6f081a)
2023-10-29 00:58:11 +03:00
Weblate
80db9a7dd4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: Jordy <prive@jordyhoebergen.nl>
Co-authored-by: LeDaFeEs <leonardo.fonseca85300@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-28 22:05:58 +03:00
Bogdan
660d3d7643 Fixed: Don't reject DVDRip/DVDRemux as full DVD disk releases 2023-10-28 22:05:12 +03:00
Bogdan
d999aea36f Add missing translation in release profiles 2023-10-26 00:18:52 +03:00
Jordy
5d45f1de89 Update link to Docker instructions in readme (#9318) 2023-10-25 11:21:51 +03:00
Weblate
3e5089719c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: jianl <jianjianfengyun@126.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-22 10:53:19 +03:00
Bogdan
ec69dfaabb Bump version to 5.1.2 2023-10-22 09:37:23 +03:00
Qstick
aa13a40bad Fixed: Update movie collection status in Radarr if removed on TMDB 2023-10-21 20:11:49 -05:00
Bogdan
9b458812f1 Fixed: Set Busy Timeout for SQLite 2023-10-21 22:47:19 +03:00
Bogdan
1bdc48a889 Revert "Bump SQLite to 3.42.0 (1.0.118)"
This reverts commit e3160466e0.
2023-10-20 22:28:49 +03:00
Servarr
e5d479a162 Automated API Docs update 2023-10-17 19:13:07 +03:00
Bogdan
9a50fcb82a Sort movies by name in filter builder 2023-10-17 18:59:34 +03:00
Stevie Robinson
f2357e0b60 Fixed: Reduce font size for series title on series details
(cherry picked from commit 03f5174a4b2a005aab8d1a1540f4bcb272682f2e)

Closes #9301
Closes #9302
2023-10-17 18:23:33 +03:00
Mark McDowall
0591d05c3b New: History custom filters
(cherry picked from commit 2fe8f3084c90688e6dd01d600796396e74f43ff9)

Closes #9298
2023-10-17 18:23:07 +03:00
Mark McDowall
299d50d56c Cleanup Calendar custom filters
(cherry picked from commit fd789343b587da252461d84bafba2d72651a11df)

Closes #9296
2023-10-17 14:41:55 +03:00
Mark McDowall
7d3c01114b New: Queue custom filters
(cherry picked from commit e357d17b187378b92377f8acb077b12c1e7ea527)

Closes #9297
2023-10-17 14:38:49 +03:00
bakerboy448
70376af70b Fixed: Re-run Removed Movie health check after movie is deleted (#9277) 2023-10-17 12:39:20 +03:00
Bogdan
9ef031bd9e Fixed: Don't die when adding existing exclusions 2023-10-16 05:45:24 +03:00
Weblate
3a9b276c43 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidHenryThoreau <sorau@protonmail.com>
Co-authored-by: Dlgeri123 <bornemiszageri@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: bai0012 <baicongrui@gmail.com>
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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-15 18:33:49 +03:00
Bogdan
aabf209a07 Bump version to 5.1.1 2023-10-15 07:52:43 +03:00
176 changed files with 4692 additions and 3270 deletions

View File

@@ -2,7 +2,7 @@
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.1.0'
majorVersion: '5.2.2'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'

View File

@@ -36,6 +36,7 @@ class Blocklist extends Component {
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
isConfirmClearModalOpen: false,
items: props.items
};
}
@@ -90,6 +91,19 @@ class Blocklist extends Component {
this.setState({ isConfirmRemoveModalOpen: false });
};
onClearBlocklistPress = () => {
this.setState({ isConfirmClearModalOpen: true });
};
onClearBlocklistConfirmed = () => {
this.props.onClearBlocklistPress();
this.setState({ isConfirmClearModalOpen: false });
};
onConfirmClearModalClose = () => {
this.setState({ isConfirmClearModalOpen: false });
};
//
// Render
@@ -103,7 +117,6 @@ class Blocklist extends Component {
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
@@ -111,7 +124,8 @@ class Blocklist extends Component {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
isConfirmRemoveModalOpen,
isConfirmClearModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
@@ -131,8 +145,9 @@ class Blocklist extends Component {
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
onPress={this.onClearBlocklistPress}
/>
</PageToolbarSection>
@@ -215,6 +230,16 @@ class Blocklist extends Component {
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
<ConfirmModal
isOpen={isConfirmClearModalOpen}
kind={kinds.DANGER}
title={translate('ClearBlocklist')}
message={translate('ClearBlocklistMessageText')}
confirmLabel={translate('Clear')}
onConfirm={this.onClearBlocklistConfirmed}
onCancel={this.onConfirmClearModalClose}
/>
</PageContent>
);
}

View File

@@ -14,6 +14,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
@@ -33,6 +34,7 @@ class History extends Component {
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
onFilterSelect,
onFirstPagePress,
@@ -70,7 +72,8 @@ class History extends Component {
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@@ -144,8 +147,9 @@ History.propTypes = {
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired

View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
@@ -11,11 +12,13 @@ function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.movies,
(history, movies) => {
createCustomFiltersSelector('history'),
(history, movies, customFilters) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
customFilters,
...history
};
}

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setHistoryFilter } from 'Store/Actions/historyActions';
function createHistorySelector() {
return createSelector(
(state: AppState) => state.history.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.history.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface HistoryFilterModalProps {
isOpen: boolean;
}
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
const sectionItems = useSelector(createHistorySelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'history';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setHistoryFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueFilterModal from './QueueFilterModal';
import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
@@ -153,11 +155,16 @@ class Queue extends Component {
isMoviesPopulated,
moviesError,
columns,
selectedFilterKey,
filters,
customFilters,
count,
totalRecords,
isGrabbing,
isRemoving,
isRefreshMonitoredDownloadsExecuting,
onRefreshPress,
onFilterSelect,
...otherProps
} = this.props;
@@ -220,6 +227,15 @@ class Queue extends Component {
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={QueueFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
@@ -241,7 +257,11 @@ class Queue extends Component {
{
isAllPopulated && !hasError && !items.length ?
<Alert kind={kinds.INFO}>
{translate('QueueIsEmpty')}
{
selectedFilterKey !== 'all' && count > 0 ?
translate('QueueFilterHasNoItems') :
translate('QueueIsEmpty')
}
</Alert> :
null
}
@@ -325,13 +345,22 @@ Queue.propTypes = {
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
count: PropTypes.number.isRequired,
totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired
onRemoveSelectedPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
Queue.defaultProps = {
count: 0
};
export default Queue;

View File

@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import * as queueActions from 'Store/Actions/queueActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue';
@@ -15,12 +16,16 @@ function createMapStateToProps() {
(state) => state.movies,
(state) => state.queue.options,
(state) => state.queue.paged,
(state) => state.queue.status.item,
createCustomFiltersSelector('queue'),
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
return {
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
customFilters,
isRefreshMonitoredDownloadsExecuting,
...options,
...queue
@@ -106,6 +111,10 @@ class QueueConnector extends Component {
this.props.setQueueSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setQueueFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload);
@@ -140,6 +149,7 @@ class QueueConnector extends Component {
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress}
onGrabSelectedPress={this.onGrabSelectedPress}
@@ -162,6 +172,7 @@ QueueConnector.propTypes = {
gotoQueueLastPage: PropTypes.func.isRequired,
gotoQueuePage: PropTypes.func.isRequired,
setQueueSort: PropTypes.func.isRequired,
setQueueFilter: PropTypes.func.isRequired,
setQueueTableOption: PropTypes.func.isRequired,
clearQueue: PropTypes.func.isRequired,
grabQueueItems: PropTypes.func.isRequired,

View File

@@ -81,4 +81,9 @@ QueueDetails.propTypes = {
progressBar: PropTypes.node.isRequired
};
QueueDetails.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
export default QueueDetails;

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setQueueFilter } from 'Store/Actions/queueActions';
function createQueueSelector() {
return createSelector(
(state: AppState) => state.queue.paged.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.queue.paged.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface QueueFilterModalProps {
isOpen: boolean;
}
export default function QueueFilterModal(props: QueueFilterModalProps) {
const sectionItems = useSelector(createQueueSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'queue';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setQueueFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css';
@@ -41,8 +40,8 @@ QueueStatusCell.propTypes = {
};
QueueStatusCell.defaultProps = {
trackedDownloadStatus: translate('Ok'),
trackedDownloadState: translate('Downloading')
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
export default QueueStatusCell;

View File

@@ -1,6 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatTime from 'Utilities/Date/formatTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
@@ -25,11 +28,13 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('DelayingDownloadUntil', { date, time })}
>
-
<TableRowCell className={styles.timeleft}>
<Tooltip
anchor={<Icon name={icons.INFO} />}
tooltip={translate('DelayingDownloadUntil', { date, time })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}
@@ -39,11 +44,13 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('RetryingDownloadOn', { date, time })}
>
-
<TableRowCell className={styles.timeleft}>
<Tooltip
anchor={<Icon name={icons.INFO} />}
tooltip={translate('RetryingDownloadOn', { date, time })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@@ -130,7 +131,12 @@ class AddNewMovie extends Component {
<div className={styles.helpText}>
{translate('FailedLoadingSearchResults')}
</div>
<div>{getErrorMessage(error)}</div>
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
<div>
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
{translate('WhySearchesCouldBeFailing')}
</Link>
</div>
</div> : null
}

View File

@@ -66,6 +66,7 @@ class AddNewMovieSearchResult extends Component {
ratings,
folder,
images,
existingMovieId,
isExistingMovie,
isExclusionMovie,
isSmallScreen,
@@ -74,8 +75,7 @@ class AddNewMovieSearchResult extends Component {
monitored,
hasFile,
isAvailable,
queueStatus,
queueState,
movieFile,
runtime,
movieRuntimeFormat,
certification
@@ -120,13 +120,13 @@ class AddNewMovieSearchResult extends Component {
{
isExistingMovie &&
<MovieIndexProgressBar
movieId={existingMovieId}
movieFile={movieFile}
monitored={monitored}
hasFile={hasFile}
status={status}
width={posterWidth}
detailedProgressBar={true}
queueStatus={queueStatus}
queueState={queueState}
isAvailable={isAvailable}
/>
}
@@ -278,6 +278,7 @@ AddNewMovieSearchResult.propTypes = {
ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
existingMovieId: PropTypes.number,
isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
@@ -286,9 +287,8 @@ AddNewMovieSearchResult.propTypes = {
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
colorImpairedMode: PropTypes.bool,
queueStatus: PropTypes.string,
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string

View File

@@ -10,17 +10,15 @@ function createMapStateToProps() {
createExistingMovieSelector(),
createExclusionMovieSelector(),
createDimensionsSelector(),
(state) => state.queue.details.items,
(state, { internalId }) => internalId,
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
(state) => state.settings.ui.item.movieRuntimeFormat,
(isExistingMovie, isExclusionMovie, dimensions, internalId, movieRuntimeFormat) => {
return {
existingMovieId: internalId,
isExistingMovie,
isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen,
queueStatus: firstQueueItem ? firstQueueItem.status : null,
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
movieRuntimeFormat
};
}
);

View File

@@ -5,12 +5,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
createRootFoldersSelector(),
createSystemStatusSelector(),
(rootFolders, systemStatus) => {
return {

View File

@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AppUpdated', { appName: 'Radarr' })}
{translate('AppUpdated')}
</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} />
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
</div>
{

View File

@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostToBackend', { appName: 'Radarr' })}
{translate('ConnectionLostToBackend')}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostReconnect', { appName: 'Radarr' })}
{translate('ConnectionLostReconnect')}
</div>
</ModalBody>
<ModalFooter>

View File

@@ -1,4 +1,5 @@
import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp } from './AppState';
export interface Error {
responseJSON: {
@@ -20,6 +21,10 @@ export interface PagedAppSectionState {
pageSize: number;
}
export interface AppSectionFilterState<T> {
filterBuilderProps: FilterBuilderProp<T>[];
}
export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;

View File

@@ -1,6 +1,7 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
@@ -46,6 +47,7 @@ export interface CustomFilter {
interface AppState {
calendar: CalendarAppState;
commands: CommandAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState;
movieFiles: MovieFilesAppState;

View File

@@ -1,9 +1,10 @@
import AppSectionState from 'App/State/AppSectionState';
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import Movie from 'Movie/Movie';
import { FilterBuilderProp } from './AppState';
interface CalendarAppState extends AppSectionState<Movie> {
filterBuilderProps: FilterBuilderProp<Movie>[];
}
interface CalendarAppState
extends AppSectionState<Movie>,
AppSectionFilterState<Movie> {}
export default CalendarAppState;

View File

@@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import History from 'typings/History';
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History> {}
export default HistoryAppState;

View File

@@ -1,41 +1,17 @@
import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
export interface StatusMessage {
title: string;
messages: string[];
}
export interface Queue extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
statusMessages: StatusMessage[];
errorMessage: string;
downloadId: string;
protocol: string;
downloadClient: string;
outputPath: string;
movieHasFile: boolean;
movieId?: number;
}
import Queue from 'typings/Queue';
import AppSectionState, {
AppSectionFilterState,
AppSectionItemState,
Error,
} from './AppSectionState';
export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown;
}
export interface QueuePagedAppState extends AppSectionState<Queue> {
export interface QueuePagedAppState
extends AppSectionState<Queue>,
AppSectionFilterState<Queue> {
isGrabbing: boolean;
grabError: Error;
isRemoving: boolean;

View File

@@ -23,13 +23,11 @@ function createFilterBuilderPropsSelector() {
);
}
interface SeriesIndexFilterModalProps {
interface CalendarFilterModalProps {
isOpen: boolean;
}
export default function CalendarFilterModal(
props: SeriesIndexFilterModalProps
) {
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar';

View File

@@ -6,9 +6,12 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
@@ -58,9 +61,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue;
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
return HistoryEventTypeFilterBuilderRowValue;
case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector;
case filterBuilderValueTypes.LANGUAGE:
return LanguageFilterBuilderRowValue;
case filterBuilderValueTypes.PROTOCOL:
return ProtocolFilterBuilderRowValue;
@@ -70,6 +79,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.MOVIE:
return MovieFilterBuilderRowValue;
case filterBuilderValueTypes.RELEASE_STATUS:
return ReleaseStatusFilterBuilderRowValue;

View File

@@ -0,0 +1,16 @@
import { FilterBuilderProp } from 'App/State/AppState';
interface FilterBuilderRowOnChangeProps {
name: string;
value: unknown[];
}
interface FilterBuilderRowValueProps {
filterType?: string;
filterValue: string | number | object | string[] | number[] | object[];
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
sectionItem: unknown[];
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
}
export default FilterBuilderRowValueProps;

View File

@@ -0,0 +1,51 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
const EVENT_TYPE_OPTIONS = [
{
id: 1,
get name() {
return translate('Grabbed');
},
},
{
id: 3,
get name() {
return translate('Imported');
},
},
{
id: 4,
get name() {
return translate('Failed');
},
},
{
id: 6,
get name() {
return translate('Deleted');
},
},
{
id: 8,
get name() {
return translate('Renamed');
},
},
{
id: 9,
get name() {
return translate('Ignored');
},
},
];
function HistoryEventTypeFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
}
export default HistoryEventTypeFilterBuilderRowValue;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const { items } = useSelector(createLanguagesSelector());
return <FilterBuilderRowValue {...props} tagList={items} />;
}
export default LanguageFilterBuilderRowValue;

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Movie from 'Movie/Movie';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import sortByName from 'Utilities/Array/sortByName';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const allMovies: Movie[] = useSelector(createAllMoviesSelector());
const tagList = allMovies
.map((movie) => ({ id: movie.id, name: movie.title }))
.sort(sortByName);
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}
export default MovieFilterBuilderRowValue;

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import translate from 'Utilities/String/translate';
import RootFolderSelectInput from './RootFolderSelectInput';
@@ -10,7 +11,7 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
createRootFoldersSelector(),
(state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange,

View File

@@ -78,7 +78,8 @@ function createMapDispatchToProps(dispatch, props) {
onImportListSyncPress() {
dispatch(executeCommand({
name: commandNames.IMPORT_LIST_SYNC
name: commandNames.IMPORT_LIST_SYNC,
commandFinished: this.dispatchFetchListMovies
}));
}
};

View File

@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod,
authenticationRequired,
username,
password
password,
passwordConfirmation
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
@@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
{translate('AuthenticationRequiredWarning')}
</Alert>
{
@@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
onChange={onInputChange}
@@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
{...password}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
{...passwordConfirmation}
/>
</FormGroup>
</div> :
null
}

View File

@@ -2,10 +2,13 @@ export const BOOL = 'bool';
export const BYTES = 'bytes';
export const DATE = 'date';
export const DEFAULT = 'default';
export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer';
export const LANGUAGE = 'language';
export const PROTOCOL = 'protocol';
export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile';
export const MOVIE = 'movie';
export const RELEASE_STATUS = 'releaseStatus';
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
export const TAG = 'tag';

View File

@@ -202,6 +202,12 @@
.headerContent {
padding: 15px;
}
.title {
font-weight: 300;
font-size: 30px;
line-height: 30px;
}
}
@media only screen and (max-width: $breakpointLarge) {

View File

@@ -286,7 +286,7 @@ class MovieDetails extends Component {
onMonitorTogglePress,
onRefreshPress,
onSearchPress,
queueItems,
queueItem,
movieRuntimeFormat
} = this.props;
@@ -544,7 +544,7 @@ class MovieDetails extends Component {
hasMovieFiles={hasMovieFiles}
monitored={monitored}
isAvailable={isAvailable}
queueItem={(queueItems.length > 0) ? queueItems[0] : null}
queueItem={queueItem}
/>
</span>
</InfoLabel>
@@ -830,7 +830,7 @@ MovieDetails.propTypes = {
onRefreshPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired,
onGoToMovie: PropTypes.func.isRequired,
queueItems: PropTypes.arrayOf(PropTypes.object),
queueItem: PropTypes.object,
movieRuntimeFormat: PropTypes.string.isRequired
};

View File

@@ -145,6 +145,8 @@ function createMapStateToProps() {
return acc;
}, []);
const queueItem = queueItems.find((item) => item.movieId === movie.id);
return {
...movie,
alternateTitles,
@@ -165,7 +167,7 @@ function createMapStateToProps() {
nextMovie,
isSmallScreen: dimensions.isSmallScreen,
isSidebarVisible,
queueItems,
queueItem,
movieRuntimeFormat
};
}

View File

@@ -8,7 +8,6 @@ import translate from 'Utilities/String/translate';
import styles from './MovieStatusLabel.css';
function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
if (queueItem) {
const queueStatus = queueItem.status;
const queueState = queueItem.trackedDownloadStatus;
@@ -116,8 +115,4 @@ MovieStatusLabel.propTypes = {
colorImpairedMode: PropTypes.bool
};
MovieStatusLabel.defaultProps = {
title: ''
};
export default MovieStatusLabel;

View File

@@ -11,7 +11,10 @@ function createMovieQueueDetailsSelector(movieId: number) {
(queueItems) => {
return queueItems.reduce(
(acc: MovieQueueDetails, item) => {
if (item.movieId !== movieId) {
if (
item.trackedDownloadState === 'imported' ||
item.movieId !== movieId
) {
return acc;
}

View File

@@ -124,6 +124,7 @@ class SecuritySettings extends Component {
authenticationRequired,
username,
password,
passwordConfirmation,
apiKey,
certificateValidation
} = settings;
@@ -139,8 +140,8 @@ class SecuritySettings extends Component {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={translate('AuthenticationRequiredWarning')}
onChange={onInputChange}
{...authenticationMethod}
/>
@@ -193,6 +194,21 @@ class SecuritySettings extends Component {
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
{...passwordConfirmation}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>{translate('ApiKey')}</FormLabel>

View File

@@ -83,7 +83,7 @@ function UpdateSettings(props) {
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Radarr' }) : undefined}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>

View File

@@ -74,9 +74,15 @@ class ImportList extends Component {
<div className={styles.enabled}>
{
enabled &&
enabled ?
<Label kind={kinds.SUCCESS}>
{translate('Enabled')}
</Label> :
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
@@ -86,16 +92,6 @@ class ImportList extends Component {
{translate('Auto')}
</Label>
}
{
!enabled && !enableAuto &&
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
</div>
<div className={styles.enabled}>

View File

@@ -12,6 +12,7 @@ import translate from 'Utilities/String/translate';
import styles from './ManageImportListsEditModalContent.css';
interface SavePayload {
enabled?: boolean;
enableAuto?: boolean;
qualityProfileId?: number;
rootFolderPath?: string;
@@ -25,7 +26,7 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange';
const autoAddOptions = [
const enableOptions = [
{
key: NO_CHANGE,
get value() {
@@ -52,6 +53,7 @@ function ManageImportListsEditModalContent(
) {
const { importListIds, onSavePress, onModalClose } = props;
const [enabled, setEnabled] = useState(NO_CHANGE);
const [enableAuto, setEnableAuto] = useState(NO_CHANGE);
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
NO_CHANGE
@@ -62,6 +64,11 @@ function ManageImportListsEditModalContent(
let hasChanges = false;
const payload: SavePayload = {};
if (enabled !== NO_CHANGE) {
hasChanges = true;
payload.enabled = enabled === 'enabled';
}
if (enableAuto !== NO_CHANGE) {
hasChanges = true;
payload.enableAuto = enableAuto === 'enabled';
@@ -82,11 +89,21 @@ function ManageImportListsEditModalContent(
}
onModalClose();
}, [enableAuto, qualityProfileId, rootFolderPath, onSavePress, onModalClose]);
}, [
enabled,
enableAuto,
qualityProfileId,
rootFolderPath,
onSavePress,
onModalClose,
]);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'enabled':
setEnabled(value);
break;
case 'enableAuto':
setEnableAuto(value);
break;
@@ -110,6 +127,18 @@ function ManageImportListsEditModalContent(
<ModalHeader>{translate('EditSelectedImportLists')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormLabel>{translate('Enabled')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enabled"
value={enabled}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AutomaticAdd')}</FormLabel>
@@ -117,7 +146,7 @@ function ManageImportListsEditModalContent(
type={inputTypes.SELECT}
name="enableAuto"
value={enableAuto}
values={autoAddOptions}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -58,6 +58,12 @@ const COLUMNS = [
isSortable: true,
isVisible: true,
},
{
name: 'enabled',
label: () => translate('Enabled'),
isSortable: true,
isVisible: true,
},
{
name: 'enableAuto',
label: () => translate('AutomaticAdd'),

View File

@@ -1,5 +1,6 @@
.name,
.tags,
.enabled,
.enableAuto,
.qualityProfileId,
.rootFolderPath,
@@ -7,4 +8,4 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'enableAuto': string;
'enabled': string;
'implementation': string;
'name': string;
'qualityProfileId': string;

View File

@@ -7,6 +7,7 @@ import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector';
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import styles from './ManageImportListsModalRow.css';
interface ManageImportListsModalRowProps {
@@ -16,6 +17,7 @@ interface ManageImportListsModalRowProps {
qualityProfileId: number;
implementation: string;
tags: number[];
enabled: boolean;
enableAuto: boolean;
columns: Column[];
isSelected?: boolean;
@@ -30,6 +32,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
rootFolderPath,
qualityProfileId,
implementation,
enabled,
enableAuto,
tags,
onSelectedChange,
@@ -63,15 +66,19 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
</TableRowCell>
<TableRowCell className={styles.qualityProfileId}>
{qualityProfile?.name ?? 'None'}
{qualityProfile?.name ?? translate('None')}
</TableRowCell>
<TableRowCell className={styles.rootFolderPath}>
{rootFolderPath}
</TableRowCell>
<TableRowCell className={styles.enabled}>
{enabled ? translate('Yes') : translate('No')}
</TableRowCell>
<TableRowCell className={styles.enableAuto}>
{enableAuto ? 'Yes' : 'No'}
{enableAuto ? translate('Yes') : translate('No')}
</TableRowCell>
<TableRowCell className={styles.tags}>

View File

@@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState';
import { set, updateServerSideCollection } from '../baseActions';
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
const [baseSection] = section.split('.');
return function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
@@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
const {
selectedFilterKey,
filters,
customFilters
filters
} = sectionState;
const customFilters = getState().customFilters.items.filter((customFilter) => {
return customFilter.type === section || customFilter.type === baseSection;
});
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
selectedFilters.forEach((filter) => {
@@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
const promise = createAjaxRequest({
url,
data
data,
traditional: true
}).request;
promise.done((response) => {

View File

@@ -49,8 +49,6 @@ export const defaultState = {
selectedFilterKey: 'monitored',
customFilters: [],
filters: [
{
key: 'all',

View File

@@ -9,7 +9,7 @@ import getNewMovie from 'Utilities/Movie/getNewMovie';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import translate from 'Utilities/String/translate';
import { removeItem, set, updateItem } from './baseActions';
import { removeItem, set, update, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
@@ -507,11 +507,11 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
// set an Id so the selectors and updaters done blow up.
// set an ID so the selectors and updaters done blow up.
data = data.map((movie) => ({ ...movie, id: movie.tmdbId }));
dispatch(batchActions([
...data.map((movie) => updateItem({ section, ...movie })),
update({ section, data }),
set({
section,

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { createAction } from 'redux-actions';
import Icon from 'Components/Icon';
import { filterTypes, icons, sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@@ -177,6 +177,33 @@ export const defaultState = {
}
]
}
],
filterBuilderProps: [
{
name: 'eventType',
label: () => translate('EventType'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
},
{
name: 'movieIds',
label: () => translate('Movie'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.MOVIE
},
{
name: 'quality',
label: () => translate('Quality'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.QUALITY
},
{
name: 'languages',
label: () => translate('Languages'),
type: filterBuilderTypes.CONTAINS,
valueType: filterBuilderValueTypes.LANGUAGE
}
]
};

View File

@@ -28,8 +28,8 @@ export const defaultState = {
isReprocessing: false,
error: null,
items: [],
sortKey: 'quality',
sortDirection: sortDirections.DESCENDING,
sortKey: 'relativePath',
sortDirection: sortDirections.ASCENDING,
recentFolders: [],
importMode: 'chooseImportMode',
sortPredicates: {

View File

@@ -4,7 +4,7 @@ import React from 'react';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import Icon from 'Components/Icon';
import { icons, sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@@ -159,6 +159,43 @@ export const defaultState = {
isVisible: true,
isModifiable: false
}
],
selectedFilterKey: 'all',
filters: [
{
key: 'all',
label: 'All',
filters: []
}
],
filterBuilderProps: [
{
name: 'movieIds',
label: () => translate('Movie'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.MOVIE
},
{
name: 'quality',
label: () => translate('Quality'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.QUALITY
},
{
name: 'languages',
label: () => translate('Languages'),
type: filterBuilderTypes.CONTAINS,
valueType: filterBuilderValueTypes.LANGUAGE
},
{
name: 'protocol',
label: () => translate('Protocol'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.PROTOCOL
}
]
},
sortPredicates: {
@@ -173,7 +210,8 @@ export const persistState = [
'queue.paged.pageSize',
'queue.paged.sortKey',
'queue.paged.sortDirection',
'queue.paged.columns'
'queue.paged.columns',
'queue.paged.selectedFilterKey'
];
//
@@ -198,6 +236,7 @@ export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage';
export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage';
export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage';
export const SET_QUEUE_SORT = 'queue/setQueueSort';
export const SET_QUEUE_FILTER = 'queue/setQueueFilter';
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
export const CLEAR_QUEUE = 'queue/clearQueue';
@@ -222,6 +261,7 @@ export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE);
export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE);
export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE);
export const setQueueSort = createThunk(SET_QUEUE_SORT);
export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
export const setQueueOption = createAction(SET_QUEUE_OPTION);
export const clearQueue = createAction(CLEAR_QUEUE);
@@ -268,7 +308,8 @@ export const actionHandlers = handleThunks({
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE,
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE,
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE,
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT,
[serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER
},
fetchDataAugmenter
),

View File

@@ -70,7 +70,7 @@ module.exports = {
// Toolbar
toolbarColor: '#e1e2e3',
toolbarBackgroundColor: '#262626',
toolbarMenuItemBackgroundColor: '#606060',
toolbarMenuItemBackgroundColor: '#303030',
toolbarMenuItemHoverBackgroundColor: '#515151',
toolbarLabelColor: '#e1e2e3',

View File

@@ -116,6 +116,7 @@ class BackupRow extends Component {
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RestoreBackup')}
name={icons.RESTORE}
onPress={this.onRestorePress}
/>
@@ -138,7 +139,9 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', { name })}
message={translate('DeleteBackupMessageText', {
name
})}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose}

View File

@@ -109,7 +109,7 @@ class Backups extends Component {
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadBackups')}
{translate('BackupsLoadError')}
</Alert>
}

View File

@@ -146,7 +146,9 @@ class RestoreBackupModalContent extends Component {
<ModalBody>
{
!!id && translate('WouldYouLikeToRestoreBackup', { name })
!!id && translate('WouldYouLikeToRestoreBackup', {
name
})
}
{

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -67,7 +67,7 @@ class LogFiles extends Component {
/>
<PageToolbarButton
label={translate('Delete')}
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={deleteFilesExecuting}
onPress={onDeleteFilesPress}
@@ -77,13 +77,15 @@ class LogFiles extends Component {
<PageContentBody>
<Alert>
<div>
Log files are located in: {location}
{translate('LogFilesLocation', {
location
})}
</div>
{
currentLogView === 'Log Files' &&
<div>
{translate('TheLogLevelDefault')} <Link to="/settings/general">{translate('GeneralSettings')}</Link>
<InlineMarkdown data={translate('TheLogLevelDefault')} />
</div>
}
</Alert>

View File

@@ -4,6 +4,7 @@ import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';
import translate from 'Utilities/String/translate';
class LogsNavMenu extends Component {
@@ -50,13 +51,13 @@ class LogsNavMenu extends Component {
<MenuItem
to={'/system/logs/files'}
>
Log Files
{translate('LogFiles')}
</MenuItem>
<MenuItem
to={'/system/logs/files/update'}
>
Updater Log Files
{translate('UpdaterLogFiles')}
</MenuItem>
</MenuContent>
</Menu>

View File

@@ -45,7 +45,14 @@ class About extends Component {
packageVersion &&
<DescriptionListItem
title={translate('PackageVersion')}
data={(packageAuthor ? <span> {packageVersion} {' by '} <InlineMarkdown data={packageAuthor} /> </span> : packageVersion)}
data={(packageAuthor ?
<InlineMarkdown data={translate('PackageVersionInfo', {
packageVersion,
packageAuthor
})}
/> :
packageVersion
)}
/>
}

View File

@@ -153,7 +153,7 @@ class Health extends Component {
{
!healthIssues &&
<div className={styles.healthOk}>
{translate('HealthNoIssues')}
{translate('NoIssuesWithYourConfiguration')}
</div>
}

View File

@@ -26,7 +26,7 @@ class MoreInfo extends Component {
{translate('Wiki')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://wiki.servarr.com/radarr">{translate('Wiki')}</Link>
<Link to="https://wiki.servarr.com/radarr">wiki.servarr.com/radarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>

View File

@@ -44,7 +44,7 @@ class Updates extends Component {
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterPrefix = translate('UnableToUpdateRadarrDirectly');
const externalUpdaterPrefix = translate('UpdateRadarrDirectlyLoadError');
const externalUpdaterMessages = {
external: translate('ExternalUpdater'),
apt: translate('AptUpdater'),
@@ -176,7 +176,7 @@ class Updates extends Component {
kind={kinds.INVERSE}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
>
Previously Installed
{translate('PreviouslyInstalled')}
</Label> :
null
}
@@ -213,14 +213,14 @@ class Updates extends Component {
{
!!updatesError &&
<div>
Failed to fetch updates
{translate('FailedToFetchUpdates')}
</div>
}
{
!!generalSettingsError &&
<div>
Failed to update settings
{translate('FailedToUpdateSettings')}
</div>
}
</PageContentBody>

View File

@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate(
key: string,
tokens?: Record<string, string | number | boolean>
tokens: Record<string, string | number | boolean> = {}
) {
const translation = translations[key] || key;
if (tokens) {
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
tokens.appName = 'Radarr';
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
return translation;
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}

View File

@@ -0,0 +1,27 @@
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from './CustomFormat';
export type HistoryEventType =
| 'grabbed'
| 'downloadFolderImported'
| 'downloadFailed'
| 'movieFileDeleted'
| 'movieFolderImported'
| 'movieFileRenamed'
| 'downloadIgnored';
export default interface History {
movieId: number;
sourceTitle: string;
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
customFormatScore: number;
qualityCutoffNotMet: boolean;
date: string;
downloadId: string;
eventType: HistoryEventType;
data: unknown;
id: number;
}

View File

@@ -12,6 +12,7 @@ export interface Field {
interface ImportList extends ModelBase {
enable: boolean;
enabled: boolean;
enableAuto: boolean;
qualityProfileId: number;
rootFolderPath: string;

View File

@@ -0,0 +1,43 @@
import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
export type QueueTrackedDownloadStatus = 'ok' | 'warning' | 'error';
export type QueueTrackedDownloadState =
| 'downloading'
| 'importPending'
| 'importing'
| 'imported'
| 'failedPending'
| 'failed'
| 'ignored';
export interface StatusMessage {
title: string;
messages: string[];
}
interface Queue extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: QueueTrackedDownloadStatus;
trackedDownloadState: QueueTrackedDownloadState;
statusMessages: StatusMessage[];
errorMessage: string;
downloadId: string;
protocol: string;
downloadClient: string;
outputPath: string;
movieHasFile: boolean;
movieId?: number;
}
export default Queue;

View File

@@ -16,7 +16,7 @@
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.8" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.118-22" />
<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.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />

View File

@@ -86,5 +86,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_remoteMovie.Release.Title = title;
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
}
[TestCase("Series Title EP50 USLT NTSC DVDRemux DD2.0")]
[TestCase("Series.Title.S01.NTSC.DVDRip.DD2.0.x264-PLAiD")]
public void should_return_true_if_dvdrip(string title)
{
_remoteMovie.Release.Title = title;
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
}
}
}

View File

@@ -68,5 +68,16 @@ namespace NzbDrone.Core.Test.IndexerTests
VerifyNoUpdate();
}
[Test]
public void should_not_record_failure_for_unknown_provider()
{
Subject.RecordFailure(0);
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.FindByProviderId(1), Times.Never);
VerifyNoUpdate();
}
}
}

View File

@@ -24,35 +24,28 @@ namespace NzbDrone.Core.Test.Localization
[Test]
public void should_get_string_in_dictionary_if_lang_exists_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("UiLanguage");
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("UI Language");
}
[Test]
public void should_get_string_in_default_language_dictionary_if_no_lang_country_code_exists_and_string_exists()
public void should_get_string_in_french()
{
var localizedString = Subject.GetLocalizedString("UiLanguage", "fr_fr");
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.French);
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("Langue de l'IU");
ExceptionVerification.ExpectedErrors(1);
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists()
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("UiLanguage", "an");
localizedString.Should().Be("UI Language");
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("UiLanguage", "");
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns(0);
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("UI Language");
}
@@ -60,7 +53,7 @@ namespace NzbDrone.Core.Test.Localization
[Test]
public void should_return_argument_if_string_doesnt_exists()
{
var localizedString = Subject.GetLocalizedString("badString", "en");
var localizedString = Subject.GetLocalizedString("badString");
localizedString.Should().Be("badString");
}

View File

@@ -37,6 +37,12 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[TestCase("The Mist", "M", "The Mist")]
[TestCase("A", "A", "A")]
[TestCase("30 Rock", "3", "30 Rock")]
[TestCase("The '80s Greatest", "8", "The '80s Greatest")]
[TestCase("좀비버스", "좀", "좀비버스")]
[TestCase("¡Mucha Lucha!", "M", "¡Mucha Lucha!")]
[TestCase(".hack", "H", "hack")]
[TestCase("Ütopya", "U", "Ütopya")]
[TestCase("Æon Flux", "A", "Æon Flux")]
public void should_get_expected_folder_name_back(string title, string parent, string child)
{
_movie.Title = title;

View File

@@ -219,6 +219,7 @@ namespace NzbDrone.Core.Test.ParserTests
}
[TestCase("Movie.Title.1994.Vietnamese.1080p.XviD-LOL")]
[TestCase("Movie.Title.1994.VIE.1080p.XviD-LOL")]
public void should_parse_language_vietnamese(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);

View File

@@ -5,7 +5,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.143" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.118-22" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Analytics
{
get
{
var lastRecord = _historyService.Paged(new PagingSpec<MovieHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
var lastRecord = _historyService.Paged(new PagingSpec<MovieHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }, null, null);
var monthAgo = DateTime.UtcNow.AddMonths(-1);
return lastRecord.Records.Any(v => v.Date > monthAgo);

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Validation;
@@ -27,7 +28,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
{
return movie.MovieMetadata.Value.Genres.Any(genre => Value.Contains(genre));
return movie?.MovieMetadata?.Value?.Genres.Any(genre => Value.ContainsIgnoreCase(genre)) ?? false;
}
public override NzbDroneValidationResult Validate()

View File

@@ -323,6 +323,20 @@ namespace NzbDrone.Core.Configuration
}
}
public void MigrateConfigFile()
{
if (!File.Exists(_configFile))
{
return;
}
// If SSL is enabled and a cert hash is still in the config file disable SSL
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
{
SetValue("EnableSsl", false);
}
}
private void DeleteOldValues()
{
var xDoc = LoadConfigFile();
@@ -394,6 +408,7 @@ namespace NzbDrone.Core.Configuration
public void HandleAsync(ApplicationStartedEvent message)
{
MigrateConfigFile();
EnsureDefaultConfigFile();
DeleteOldValues();
}

View File

@@ -162,7 +162,7 @@ namespace NzbDrone.Core.CustomFormats
}
}
return matches;
return matches.OrderBy(x => x.Name).ToList();
}
private static List<CustomFormat> ParseCustomFormat(MovieFile movieFile, Movie movie, List<CustomFormat> allCustomFormats)

View File

@@ -407,7 +407,7 @@ namespace NzbDrone.Core.Datastore
return pagingSpec;
}
private void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
protected void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
{
var filters = pagingSpec.FilterExpressions;

View File

@@ -41,14 +41,16 @@ namespace NzbDrone.Core.Datastore
private static string GetConnectionString(string dbPath)
{
var connectionBuilder = new SQLiteConnectionStringBuilder();
connectionBuilder.DataSource = dbPath;
connectionBuilder.CacheSize = (int)-20000;
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
connectionBuilder.Pooling = true;
connectionBuilder.Version = 3;
var connectionBuilder = new SQLiteConnectionStringBuilder
{
DataSource = dbPath,
CacheSize = (int)-20000,
DateTimeKind = DateTimeKind.Utc,
JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal,
Pooling = true,
Version = 3,
BusyTimeout = 100
};
if (OsInfo.IsOsx)
{

View File

@@ -13,7 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{
new Regex(@"(?:dis[ck])(?:[-_. ]\d+[-_. ])(?:(?:(?:480|720|1080|2160)[ip]|)[-_. ])?(?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:(?:480|720|1080|2160)[ip]|)[-_. ](?:full)[-_. ](?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:\d?x?M?DVD-?[R59])", RegexOptions.Compiled | RegexOptions.IgnoreCase)
new Regex(@"(?:\d?x?M?DVD-?[R59])(?:[ ._]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
};
private static readonly string[] _dvdContainerTypes = new[] { "vob", "iso" };

View File

@@ -1,4 +1,5 @@
using NLog;
using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Lifecycle;
@@ -28,7 +29,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
_logger.Warn("Please update your API key to be at least {0} characters long. You can do this via settings or the config file", MinimumLength);
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage"), MinimumLength), "#invalid-api-key");
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary<string, object> { { "length", MinimumLength } }), "#invalid-api-key");
}
return new HealthCheck(GetType());

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Download;
@@ -42,8 +43,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
_logger.Debug(ex, "Unable to communicate with {0}", downloadClient.Definition.Name);
var message = string.Format(_localizationService.GetLocalizedString("DownloadClientCheckUnableToCommunicateMessage"), downloadClient.Definition.Name);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"{message} {ex.Message}", "#unable-to-communicate-with-download-client");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("DownloadClientCheckUnableToCommunicateMessage", new Dictionary<string, object>
{
{ "downloadClientName", downloadClient.Definition.Name },
{ "errorMessage", ex.Message }
}),
"#unable-to-communicate-with-download-client");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
@@ -44,7 +45,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("DownloadClientRemovesCompletedDownloadsHealthCheckMessage"), clientName, "Radarr"),
_localizationService.GetLocalizedString("DownloadClientRemovesCompletedDownloadsHealthCheckMessage", new Dictionary<string, object>
{
{ "downloadClientName", clientName }
}),
"#download-client-removes-completed-downloads");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NLog;
@@ -52,7 +53,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
foreach (var folder in folders)
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot"), client.Definition.Name, folder.FullPath), "#downloads-in-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot", new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#downloads-in-root-folder");
}
}
catch (DownloadClientException ex)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
@@ -43,7 +44,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (status.SortingMode.IsNotNullOrWhiteSpace())
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientSortingCheckMessage"), clientName, status.SortingMode), "#download-folder-and-library-folder-not-different-folders");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("DownloadClientSortingCheckMessage", new Dictionary<string, object>
{
{ "downloadClientName", clientName },
{ "sortingMode", status.SortingMode }
}),
"#download-folder-and-library-folder-not-different-folders");
}
}
catch (DownloadClientException ex)

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download;
@@ -40,7 +41,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("DownloadClientStatusCheckAllClientMessage"), "#download-clients-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download-clients-are-unavailable-due-to-failures");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "downloadClientNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#download-clients-are-unavailable-due-to-failures");
}
}
}

View File

@@ -54,11 +54,23 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (missingRootFolders.Count == 1)
{
var missingRootFolder = missingRootFolders.First();
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ImportListMissingRoot"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), "#import-list-missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ImportListMissingRoot", new Dictionary<string, object>
{
{ "rootFolderInfo", FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value) }
}),
"#import-list-missing-root-folder");
}
var message = string.Format(_localizationService.GetLocalizedString("ImportListMultipleMissingRoots"), string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))));
return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#import-list-missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ImportListMultipleMissingRoots", new Dictionary<string, object>
{
{ "rootFoldersInfo", string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))) }
}),
"#import-list-missing-root-folder");
}
return new HealthCheck(GetType());

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.ImportLists;
@@ -40,7 +41,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("ImportListStatusCheckAllClientMessage"), "#lists-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ImportListStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.ImportList.Definition.Name))), "#lists-are-unavailable-due-to-failures");
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString("ImportListStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "importListNames", string.Join(", ", backOffProviders.Select(v => v.ImportList.Definition.Name)) }
}),
"#import-lists-are-unavailable-due-to-failures");
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
@@ -35,7 +36,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage"), string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray())),
_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray()) }
}),
"#invalid-indexer-download-client-setting");
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
@@ -41,8 +42,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerJackettAll"),
string.Join(", ", jackettAllProviders.Select(i => i.Name))),
_localizationService.GetLocalizedString("IndexerJackettAll", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", jackettAllProviders.Select(i => i.Name)) }
}),
"#jackett-all-endpoint-used");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
@@ -50,8 +51,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#indexers-are-unavailable-due-to-failures");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
@@ -48,8 +49,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage", new Dictionary<string, object>
{
{ "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#indexers-are-unavailable-due-to-failures");
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Localization;
@@ -45,7 +46,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage", new Dictionary<string, object>
{
{ "notificationNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) }
}),
"#notifications-are-unavailable-due-to-failures");
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
@@ -40,7 +41,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (!addresses.Any())
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname), "#proxy-failed-resolve-ip");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage", new Dictionary<string, object>
{
{ "proxyHostName", _configService.ProxyHostname }
}),
"#proxy-failed-resolve-ip");
}
var request = _cloudRequestBuilder.Create()
@@ -55,13 +62,27 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode), "#proxy-failed-test");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage", new Dictionary<string, object>
{
{ "statusCode", response.StatusCode }
}),
"#proxy-failed-test");
}
}
catch (Exception ex)
{
_logger.Error(ex, "Proxy Health Check failed");
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url), "#proxy-failed-test");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage", new Dictionary<string, object>
{
{ "url", request.Url }
}),
"#proxy-failed-test");
}
return new HealthCheck(GetType());

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
@@ -31,7 +32,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (!_diskProvider.FolderWritable(recycleBin))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RecycleBinUnableToWriteHealthCheck"), recycleBin), "#cannot-write-recycle-bin");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RecycleBinUnableToWriteHealthCheck", new Dictionary<string, object>
{
{ "path", recycleBin }
}),
"#cannot-write-recycle-bin");
}
return new HealthCheck(GetType());

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NLog;
@@ -69,30 +70,92 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckWrongOSPath", new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath },
{ "osName", _osInfo.Name }
}),
"#bad-remote-path-mapping");
}
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckBadDockerPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckBadDockerPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath },
{ "osName", _osInfo.Name }
}),
"#docker-bad-remote-path-mapping");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-download-client-settings");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckLocalWrongOSPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath },
{ "osName", _osInfo.Name }
}),
"#bad-download-client-settings");
}
if (!_diskProvider.FolderExists(folder.FullPath))
{
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDockerFolderMissing"), client.Definition.Name, folder.FullPath), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckDockerFolderMissing",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#docker-bad-remote-path-mapping");
}
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalFolderMissing"), client.Definition.Name, folder.FullPath), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckLocalFolderMissing",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#bad-remote-path-mapping");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckGenericPermissions"), client.Definition.Name, folder.FullPath), "#permissions-error");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckGenericPermissions",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", folder.FullPath }
}),
"#permissions-error");
}
}
}
@@ -130,12 +193,28 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (_diskProvider.FileExists(moviePath))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDownloadPermissions"), moviePath), "#permissions-error");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckDownloadPermissions",
new Dictionary<string, object>
{
{ "path", moviePath }
}),
"#permissions-error");
}
// If the file doesn't exist but MovieInfo is not null then the message is coming from
// ImportApprovedMovies and the file must have been removed part way through processing
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFileRemoved"), moviePath), "#remote-path-file-removed");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFileRemoved",
new Dictionary<string, object>
{
{ "path", moviePath }
}),
"#remote-path-file-removed");
}
// If the previous case did not match then the failure occured in DownloadedMovieImportService,
@@ -157,42 +236,118 @@ namespace NzbDrone.Core.HealthCheck.Checks
// that the user realises something is wrong.
if (dlpath.IsNullOrWhiteSpace())
{
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("RemotePathMappingCheckImportFailed"), "#remote-path-import-failed");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RemotePathMappingCheckImportFailed"),
"#remote-path-import-failed");
}
if (!dlpath.IsPathValid(PathValidationType.CurrentOs))
{
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesWrongOSPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}),
"#bad-remote-path-mapping");
}
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesBadDockerPath"), client.Definition.Name, dlpath, _osInfo.Name), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesBadDockerPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}),
"#docker-bad-remote-path-mapping");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesLocalWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-download-client-settings");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesLocalWrongOSPath",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}),
"#bad-download-client-settings");
}
if (_diskProvider.FolderExists(dlpath))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFolderPermissions"), dlpath), "#permissions-error");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFolderPermissions",
new Dictionary<string, object>
{
{ "path", dlpath }
}),
"#permissions-error");
}
// if it's a remote client/docker, likely missing path mappings
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFolderPermissions"), client.Definition.Name, dlpath), "#docker-bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFolderPermissions",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath }
}),
"#docker-bad-remote-path-mapping");
}
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckRemoteDownloadClient"), client.Definition.Name, dlpath), "#bad-remote-path-mapping");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckRemoteDownloadClient",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath },
{ "osName", _osInfo.Name }
}), "#bad-remote-path-mapping");
}
// path mappings shouldn't be needed locally so probably a permissions issue
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesGenericPermissions"), client.Definition.Name, dlpath), "#permissions-error");
return new HealthCheck(
GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RemotePathMappingCheckFilesGenericPermissions",
new Dictionary<string, object>
{
{ "downloadClientName", client.Definition.Name },
{ "path", dlpath }
}),
"#permissions-error");
}
catch (DownloadClientException ex)
{

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Localization;
@@ -7,7 +8,7 @@ using NzbDrone.Core.Movies.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(MovieUpdatedEvent))]
[CheckOn(typeof(MoviesDeletedEvent), CheckOnCondition.FailedOnly)]
[CheckOn(typeof(MoviesDeletedEvent))]
[CheckOn(typeof(MovieRefreshCompleteEvent))]
public class RemovedMovieCheck : HealthCheckBase, ICheckOnCondition<MovieUpdatedEvent>, ICheckOnCondition<MoviesDeletedEvent>
{
@@ -32,10 +33,22 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (deletedMovie.Count == 1)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemovedMovieCheckSingleMessage"), movieText), "#movie-was-removed-from-tmdb");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RemovedMovieCheckSingleMessage", new Dictionary<string, object>
{
{ "movie", movieText }
}),
"#movie-was-removed-from-tmdb");
}
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemovedMovieCheckMultipleMessage"), movieText), "#movie-was-removed-from-tmdb");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("RemovedMovieCheckMultipleMessage", new Dictionary<string, object>
{
{ "movies", movieText }
}),
"#movie-was-removed-from-tmdb");
}
public bool ShouldCheckOnEvent(MoviesDeletedEvent message)

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
@@ -40,11 +41,26 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
if (missingRootFolders.Count == 1)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RootFolderCheckSingleMessage"), missingRootFolders.First()), "#missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RootFolderCheckSingleMessage",
new Dictionary<string, object>
{
{ "rootFolderPath", missingRootFolders.First() }
}),
"#missing-root-folder");
}
var message = string.Format(_localizationService.GetLocalizedString("RootFolderCheckMultipleMessage"), string.Join(" | ", missingRootFolders));
return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#missing-root-folder");
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString(
"RootFolderCheckMultipleMessage",
new Dictionary<string, object>
{
{ "rootFolderPaths", string.Join(" | ", missingRootFolders) }
}),
"#missing-root-folder");
}
return new HealthCheck(GetType());

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
@@ -47,7 +48,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupTranslocationMessage"), startupFolder),
_localizationService.GetLocalizedString(
"UpdateCheckStartupTranslocationMessage",
new Dictionary<string, object>
{
{ "startupFolder", startupFolder }
}),
"#cannot-install-update-because-startup-folder-is-in-an-app-translocation-folder.");
}
@@ -55,7 +61,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupNotWritableMessage"), startupFolder, Environment.UserName),
_localizationService.GetLocalizedString(
"UpdateCheckStartupNotWritableMessage",
new Dictionary<string, object>
{
{ "startupFolder", startupFolder },
{ "userName", Environment.UserName }
}),
"#cannot-install-update-because-startup-folder-is-not-writable-by-the-user");
}
@@ -63,7 +75,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
string.Format(_localizationService.GetLocalizedString("UpdateCheckUINotWritableMessage"), uiFolder, Environment.UserName),
_localizationService.GetLocalizedString(
"UpdateCheckUINotWritableMessage",
new Dictionary<string, object>
{
{ "uiFolder", uiFolder },
{ "userName", Environment.UserName }
}),
"#cannot-install-update-because-ui-folder-is-not-writable-by-the-user");
}
}

View File

@@ -19,6 +19,7 @@ namespace NzbDrone.Core.History
void DeleteForMovies(List<int> movieIds);
MovieHistory MostRecentForMovie(int movieId);
List<MovieHistory> Since(DateTime date, MovieHistoryEventType? eventType);
PagingSpec<MovieHistory> GetPaged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities);
}
public class HistoryRepository : BasicRepository<MovieHistory>, IHistoryRepository
@@ -74,19 +75,6 @@ namespace NzbDrone.Core.History
Delete(c => movieIds.Contains(c.MovieId));
}
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
.Join<MovieHistory, Movie>((h, m) => h.MovieId == m.Id)
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
.LeftJoin<Movie, MovieMetadata>((m, mm) => m.MovieMetadataId == mm.Id);
protected override IEnumerable<MovieHistory> PagedQuery(SqlBuilder sql) =>
_database.QueryJoined<MovieHistory, Movie, QualityProfile>(sql, (hist, movie, profile) =>
{
hist.Movie = movie;
hist.Movie.QualityProfile = profile;
return hist;
});
public MovieHistory MostRecentForMovie(int movieId)
{
return Query(x => x.MovieId == movieId).MaxBy(h => h.Date);
@@ -106,5 +94,77 @@ namespace NzbDrone.Core.History
return PagedQuery(builder).OrderBy(h => h.Date).ToList();
}
public PagingSpec<MovieHistory> GetPaged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
{
pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, languages, qualities), pagingSpec, PagedQuery);
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, languages, qualities).Select(typeof(MovieHistory)), pagingSpec, countTemplate);
return pagingSpec;
}
private SqlBuilder PagedBuilder(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
{
var builder = Builder()
.Join<MovieHistory, Movie>((h, m) => h.MovieId == m.Id)
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
.LeftJoin<Movie, MovieMetadata>((m, mm) => m.MovieMetadataId == mm.Id);
AddFilters(builder, pagingSpec);
if (languages is { Length: > 0 })
{
builder.Where($"({BuildLanguageWhereClause(languages)})");
}
if (qualities is { Length: > 0 })
{
builder.Where($"({BuildQualityWhereClause(qualities)})");
}
return builder;
}
protected override IEnumerable<MovieHistory> PagedQuery(SqlBuilder builder) =>
_database.QueryJoined<MovieHistory, Movie, QualityProfile>(builder, (hist, movie, profile) =>
{
hist.Movie = movie;
hist.Movie.QualityProfile = profile;
return hist;
});
private string BuildLanguageWhereClause(int[] languages)
{
var clauses = new List<string>();
foreach (var language in languages)
{
// There are 4 different types of values we should see:
// - Not the last value in the array
// - When it's the last value in the array and on different OSes
// - When it was converted from a single language
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language},%]'");
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(13) || '%]'");
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(10) || '%]'");
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[{language}]'");
}
return $"({string.Join(" OR ", clauses)})";
}
private string BuildQualityWhereClause(int[] qualities)
{
var clauses = new List<string>();
foreach (var quality in qualities)
{
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Quality\" LIKE '%_quality_: {quality},%'");
}
return $"({string.Join(" OR ", clauses)})";
}
}
}

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