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

Compare commits

...

64 Commits

Author SHA1 Message Date
Bogdan
bdc4aade0f Use extra release fields in PassThePopcorn parser 2023-12-24 06:56:48 +02:00
Weblate
b2300dbf41 Multiple Translations updated by Weblate
ignore-downstream

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

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

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

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

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

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

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

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

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

OZnzb and NZB Tortuga are dead so removed it from the presets list.
2023-12-10 15:14:13 +02:00
Bogdan
3ec3358728 Bump version to 5.2.5 2023-12-10 13:47:06 +02:00
Weblate
d4072cdfe2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Jurriaan Den Toonder <jur.den.toonder@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translation: Servarr/Radarr
2023-12-08 18:00:35 +02:00
Weblate
136a030c07 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Hajiroxx <luypanda@163.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/zh_CN/
Translation: Servarr/Radarr
2023-12-08 16:03:26 +02:00
Weblate
6d89ae89a4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Augusto Poletti <augustpolet@gmail.com>
Co-authored-by: Dominika Matějková <dominika.matejkova@outlook.cz>
Co-authored-by: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 米大饭 <1246333567@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-06 16:01:05 +02:00
Servarr
98e4273b7a Automated API Docs update 2023-12-06 16:00:42 +02:00
Bogdan
ecf9983ea6 Fix minimum availability label in movie table row 2023-12-06 14:35:55 +02:00
Bogdan
a059a700eb New: Minimum Availability in bulk manage import lists
Fixes #9461
2023-12-06 14:35:20 +02:00
Taloth Saldono
ced624c2ff Small helper in UI to access Radarr API more easily
(cherry picked from commit 090cdc364ef335fbfea8cf540696af813f6ecea4)
2023-12-06 13:16:02 +02:00
Bogdan
7c32061e17 Add existing flag for Discover Movie Posters 2023-12-06 13:12:49 +02:00
Bogdan
bc4847cdc7 New: Improve fields selection for Discord connection 2023-12-06 11:03:57 +02:00
Bogdan
65d79dd078 Fixed: Progress bar for collection movies in queue 2023-12-04 13:17:29 +02:00
Bogdan
238ddbbe1f Fixed: Improve Required Flags selection for indexers 2023-12-04 12:53:33 +02:00
Bogdan
3f444406da Fixed: (PassThePopcorn) Support for half leech releases 2023-12-04 12:34:22 +02:00
Bogdan
d7aaa1cdc2 Fixed: Cleanup orphaned movies 2023-12-03 22:02:56 +02:00
Mark McDowall
263534717d Always validate Custom Script path
(cherry picked from commit c922cc5dc617dd776d4523cbf62376821c5a4ad9)
2023-12-03 20:03:07 +02:00
Weblate
073d15160d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Patatra <patrice.chevreau@gmail.com>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: liimee <git.taaa@fedora.email>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translation: Servarr/Radarr
2023-12-01 04:03:43 +02:00
Stevie Robinson
c5075e5d49 Fixed Custom Format Deletion confirmation message
(cherry picked from commit b76bf373717edff8e475fde31fbaec86c65903fe)

Closes #9410
2023-11-26 08:43:02 +02:00
Weblate
fc345047ee Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Appoxo <appoxo@appoxo.de>
Co-authored-by: Charlie <zola@zipmail.pw>
Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-11-26 08:28:12 +02:00
Weblate
bffab87da7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-11-26 08:26:37 +02:00
Bogdan
a8a9d3b833 Bump version to 5.2.4 2023-11-26 07:05:58 +02:00
Stevie Robinson
ff1987be84 New: Remove defunct Boxcar notifications
(cherry picked from commit c6ad2396bb98dc8eb1ad47bf5d066b227a47f8b5)

Closes #9451
2023-11-25 22:29:33 +02:00
Bogdan
cb08c0767d Switch assignment to operator 2023-11-22 21:50:29 +02:00
Bogdan
5f1d7ddc11 Improve discover movies sorting by release dates 2023-11-21 22:45:23 +02:00
Bogdan
0ba3c08ea6 Small UI fixes to discover movies 2023-11-21 22:37:36 +02:00
Bogdan
6b9a378eaf Fixed: Minimum refresh interval for import list presets 2023-11-21 19:52:58 +02:00
Bogdan
b4562e6236 Fixed: Movie grabbed history on interactive search 2023-11-21 03:45:19 +02:00
Bogdan
bbffff78ed Fixed: Disable swipe on movie details when a modal is open 2023-11-20 22:16:01 +02:00
Bogdan
740f0f1e5f Cleanup unused logic in Movie Details 2023-11-20 21:37:13 +02:00
Bogdan
45b38b44c1 Fixed: Interactive search modal on mobile 2023-11-20 07:52:48 +02:00
Mark McDowall
318d59bb99 Fixed force saving provider triggering testing
(cherry picked from commit 65cb1ccafd54479fa3fca1f1eaa4b96222b0176b)
2023-11-20 07:37:42 +02:00
Bogdan
ed54d071c4 Fixed: Wrap long lines in media info popup 2023-11-20 07:06:45 +02:00
Bogdan
cff15de4fc Translate custom format score for history actions 2023-11-20 05:48:02 +02:00
Bogdan
88c0e24c58 Fixed: Clear movie search results when navigating to another page
(cherry picked from commit 67dc8987970aa2a9eade48c02ae72be1851fa196)
2023-11-19 21:10:43 -06:00
Qstick
8e0645670b New: Rework Movie Details view 2023-11-19 21:10:43 -06:00
Qstick
40eeb31a21 New: Move Interactive search to toolbar 2023-11-19 21:10:43 -06:00
Qstick
3e534cf8bf Bump version to 5.2.3 2023-11-19 18:31:23 -06:00
177 changed files with 1861 additions and 1674 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import scrollPositions from 'Store/scrollPositions';
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
@@ -38,6 +39,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchRootFolders() {
dispatch(fetchRootFolders());
},
dispatchFetchQueueDetails() {
dispatch(fetchQueueDetails());
},
dispatchClearQueueDetails() {
dispatch(clearQueueDetails());
},
onUpdateSelectedPress(payload) {
dispatch(saveMovieCollections(payload));
},
@@ -63,10 +70,12 @@ class CollectionConnector extends Component {
componentDidMount() {
registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders();
this.props.dispatchFetchQueueDetails();
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.dispatchClearQueueDetails();
}
//
@@ -99,7 +108,9 @@ CollectionConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
onUpdateSelectedPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchClearQueueDetails: PropTypes.func.isRequired
};
export default withScrollPosition(

View File

@@ -70,6 +70,7 @@ class CollectionMovie extends Component {
hasFile,
folder,
isAvailable,
movieFile,
isExistingMovie,
posterWidth,
posterHeight,
@@ -131,6 +132,8 @@ class CollectionMovie extends Component {
id ?
<div className={styles.overlayStatus}>
<MovieIndexProgressBar
movieId={id}
movieFile={movieFile}
monitored={monitored}
hasFile={hasFile}
status={status}
@@ -180,6 +183,7 @@ CollectionMovie.propTypes = {
hasFile: PropTypes.bool,
folder: PropTypes.string,
isAvailable: PropTypes.bool,
movieFile: PropTypes.object,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,

View File

@@ -1,9 +1,7 @@
.description {
line-height: $lineHeight;
}
.description {
margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
}
@media (min-width: 768px) {

View File

@@ -63,6 +63,12 @@
width: 1280px;
}
.extraExtraLarge {
composes: modal;
width: 1600px;
}
@media only screen and (max-width: $breakpointExtraLarge) {
.modal.extraLarge {
width: 90%;
@@ -90,7 +96,8 @@
.modal.small,
.modal.medium,
.modal.large,
.modal.extraLarge {
.modal.extraLarge,
.modal.extraExtraLarge {
max-height: 100%;
width: 100%;
height: 100% !important;

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'extraExtraLarge': string;
'extraLarge': string;
'large': string;
'medium': string;

View File

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

View File

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

View File

@@ -0,0 +1,120 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
// This file contains some helpers for power users in a browser console
let hasWarned = false;
function checkActivationWarning() {
if (!hasWarned) {
console.log('Activated RadarrApi console helpers.');
console.warn('Be warned: There will be no further confirmation checks.');
hasWarned = true;
}
}
function attachAsyncActions(promise) {
promise.filter = function() {
const args = arguments;
const res = this.then((d) => d.filter(...args));
attachAsyncActions(res);
return res;
};
promise.map = function() {
const args = arguments;
const res = this.then((d) => d.map(...args));
attachAsyncActions(res);
return res;
};
promise.all = function() {
const res = this.then((d) => Promise.all(d));
attachAsyncActions(res);
return res;
};
promise.forEach = function(action) {
const res = this.then((d) => Promise.all(d.map(action)));
attachAsyncActions(res);
return res;
};
}
class ResourceApi {
constructor(api, url) {
this.api = api;
this.url = url;
}
single(id) {
return this.api.fetch(`${this.url}/${id}`);
}
all() {
return this.api.fetch(this.url);
}
filter(pred) {
return this.all().filter(pred);
}
update(resource) {
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
}
delete(resource) {
if (typeof resource === 'object' && resource !== null && resource.id) {
resource = resource.id;
}
if (!resource || !Number.isInteger(resource)) {
throw Error('Invalid resource', resource);
}
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
}
fetch(url, options) {
return this.api.fetch(`${this.url}${url}`, options);
}
}
class ConsoleApi {
constructor() {
this.movie = new ResourceApi(this, '/movie');
}
resource(url) {
return new ResourceApi(this, url);
}
fetch(url, options) {
checkActivationWarning();
options = options || {};
const req = {
url,
method: options.method || 'GET'
};
if (options.data) {
req.dataType = 'json';
req.data = JSON.stringify(options.data);
}
const promise = createAjaxRequest(req).request;
promise.fail((xhr) => {
console.error(`Failed to fetch ${url}`, xhr);
});
attachAsyncActions(promise);
return promise;
}
}
window.RadarrApi = new ConsoleApi();
export default ConsoleApi;

View File

@@ -50,7 +50,7 @@ $hoverScale: 1.05;
.title {
@add-mixin truncate;
background-color: #fafbfc;
background-color: var(--movieBackgroundColor);
text-align: center;
font-size: $smallFontSize;
}
@@ -68,6 +68,19 @@ $hoverScale: 1.05;
color: var(--white);
}
.existing {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 0;
height: 0;
border-width: 25px 25px 0 0;
border-style: solid;
border-color: #37bc9b transparent transparent;
color: var(--white);
}
.controls {
position: absolute;
bottom: 10px;

View File

@@ -7,6 +7,7 @@ interface CssExports {
'controls': string;
'editorSelect': string;
'excluded': string;
'existing': string;
'externalLinks': string;
'link': string;
'overlayTitle': string;

View File

@@ -92,6 +92,7 @@ class DiscoverMoviePoster extends Component {
showRelativeDates,
shortDateFormat,
timeFormat,
movieRuntimeFormat,
...otherProps
} = this.props;
@@ -110,7 +111,7 @@ class DiscoverMoviePoster extends Component {
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
<div className={styles.posterContainer} title={title}>
{
<div className={styles.editorSelect}>
<CheckInput
@@ -158,6 +159,14 @@ class DiscoverMoviePoster extends Component {
/>
}
{
isExisting &&
<div
className={styles.existing}
title={translate('Existing')}
/>
}
<Link
className={styles.link}
style={elementStyle}
@@ -185,7 +194,7 @@ class DiscoverMoviePoster extends Component {
{
showTitle &&
<div className={styles.title}>
<div className={styles.title} title={title}>
{title}
</div>
}
@@ -194,6 +203,7 @@ class DiscoverMoviePoster extends Component {
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
movieRuntimeFormat={movieRuntimeFormat}
{...otherProps}
/>
@@ -236,6 +246,7 @@ DiscoverMoviePoster.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,

View File

@@ -5,9 +5,11 @@ import DiscoverMoviePoster from './DiscoverMoviePoster';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(),
( dimensions) => {
(movieRuntimeFormat, dimensions) => {
return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}

View File

@@ -1,5 +1,5 @@
.info {
background-color: #fafbfc;
background-color: var(--movieBackgroundColor);
text-align: center;
font-size: $smallFontSize;
}

View File

@@ -1,9 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TmdbRating from 'Components/TmdbRating';
import { icons } from 'Helpers/Props';
import { getMovieStatusDetails } from 'Movie/MovieStatus';
import formatRuntime from 'Utilities/Date/formatRuntime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
import styles from './DiscoverMoviePosterInfo.css';
function DiscoverMoviePosterInfo(props) {
@@ -19,12 +22,13 @@ function DiscoverMoviePosterInfo(props) {
sortKey,
showRelativeDates,
shortDateFormat,
timeFormat
timeFormat,
movieRuntimeFormat
} = props;
if (sortKey === 'status' && status) {
return (
<div className={styles.info}>
<div className={styles.info} title={translate('Status')}>
{getMovieStatusDetails(status).title}
</div>
);
@@ -32,7 +36,7 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'studio' && studio) {
return (
<div className={styles.info}>
<div className={styles.info} title={translate('Studio')}>
{studio}
</div>
);
@@ -50,8 +54,8 @@ function DiscoverMoviePosterInfo(props) {
);
return (
<div className={styles.info}>
{`In Cinemas ${inCinemasDate}`}
<div className={styles.info} title={translate('InCinemas')}>
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
</div>
);
}
@@ -68,8 +72,8 @@ function DiscoverMoviePosterInfo(props) {
);
return (
<div className={styles.info}>
{`Digital ${digitalReleaseDate}`}
<div className={styles.info} title={translate('DigitalRelease')}>
<Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate}
</div>
);
}
@@ -86,15 +90,15 @@ function DiscoverMoviePosterInfo(props) {
);
return (
<div className={styles.info}>
{`Released ${physicalReleaseDate}`}
<div className={styles.info} title={translate('PhysicalRelease')}>
<Icon name={icons.DISC} /> {physicalReleaseDate}
</div>
);
}
if (sortKey === 'certification' && certification) {
return (
<div className={styles.info}>
<div className={styles.info} title={translate('Certification')}>
{certification}
</div>
);
@@ -102,8 +106,8 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'runtime' && runtime) {
return (
<div className={styles.info}>
{formatRuntime(runtime)}
<div className={styles.info} title={translate('Runtime')}>
{formatRuntime(runtime, movieRuntimeFormat)}
</div>
);
}
@@ -111,9 +115,7 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'ratings' && ratings) {
return (
<div className={styles.info}>
<TmdbRating
ratings={ratings}
/>
<TmdbRating ratings={ratings} />
</div>
);
}
@@ -133,7 +135,8 @@ DiscoverMoviePosterInfo.propTypes = {
sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired
};
export default DiscoverMoviePosterInfo;

View File

@@ -76,6 +76,7 @@ class DiscoverMovieRow extends Component {
ratings,
popularity,
certification,
movieRuntimeFormat,
collection,
columns,
isExisting,
@@ -230,7 +231,7 @@ class DiscoverMovieRow extends Component {
key={name}
className={styles[name]}
>
{formatRuntime(runtime)}
{formatRuntime(runtime, movieRuntimeFormat)}
</VirtualTableRowCell>
);
}
@@ -397,6 +398,7 @@ DiscoverMovieRow.propTypes = {
popularity: PropTypes.number.isRequired,
certification: PropTypes.string,
collection: PropTypes.object,
movieRuntimeFormat: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired,

View File

@@ -5,9 +5,11 @@ import DiscoverMovieRow from './DiscoverMovieRow';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(),
(dimensions) => {
(movieRuntimeFormat, dimensions) => {
return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}

View File

@@ -3,5 +3,6 @@ export const SMALL = 'small';
export const MEDIUM = 'medium';
export const LARGE = 'large';
export const EXTRA_LARGE = 'extraLarge';
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];

View File

@@ -3,13 +3,16 @@ import React, { Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css';
import styles from './InteractiveSearch.css';
const columns = [
{
@@ -24,23 +27,6 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'title',
label: () => translate('Title'),
@@ -84,12 +70,6 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'customFormat',
label: () => translate('Formats'),
isSortable: true,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
@@ -107,10 +87,27 @@ const columns = [
}),
isSortable: true,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
}
];
function InteractiveSearchContent(props) {
function InteractiveSearch(props) {
const {
searchPayload,
isFetching,
@@ -118,18 +115,36 @@ function InteractiveSearchContent(props) {
error,
totalReleasesCount,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
onFilterSelect,
onGrabPress
} = props;
const errorMessage = getErrorMessage(error);
const type = 'movies';
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect}
/>
</div>
{
isFetching ? <LoadingIndicator /> : null
}
@@ -203,19 +218,23 @@ function InteractiveSearchContent(props) {
);
}
InteractiveSearchContent.propTypes = {
InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired,
items: 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,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default InteractiveSearchContent;
export default InteractiveSearch;

View File

@@ -2,10 +2,11 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearchContent from './InteractiveSearchContent';
import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) {
return createSelector(
@@ -29,8 +30,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(releaseActions.fetchReleases(payload));
},
dispatchClearReleases(payload) {
dispatch(releaseActions.clearReleases(payload));
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
onSortPress(sortKey, sortDirection) {
@@ -38,8 +43,7 @@ function createMapDispatchToProps(dispatch, props) {
},
onFilterSelect(selectedFilterKey) {
const action = releaseActions.setReleasesFilter;
dispatch(action({ selectedFilterKey }));
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
},
onGrabPress(payload) {
@@ -48,7 +52,7 @@ function createMapDispatchToProps(dispatch, props) {
};
}
class InteractiveSearchContentConnector extends Component {
class InteractiveSearchConnector extends Component {
//
// Lifecycle
@@ -57,7 +61,8 @@ class InteractiveSearchContentConnector extends Component {
const {
searchPayload,
isPopulated,
dispatchFetchReleases
dispatchFetchReleases,
dispatchFetchMovieHistory
} = this.props;
// If search results are not yet isPopulated fetch them,
@@ -65,6 +70,12 @@ class InteractiveSearchContentConnector extends Component {
if (!isPopulated) {
dispatchFetchReleases(searchPayload);
}
dispatchFetchMovieHistory(searchPayload);
}
componentWillUnmount() {
this.props.dispatchClearMovieHistory();
}
//
@@ -73,24 +84,26 @@ class InteractiveSearchContentConnector extends Component {
render() {
const {
dispatchFetchReleases,
dispatchClearReleases,
dispatchFetchMovieHistory,
dispatchClearMovieHistory,
...otherProps
} = this.props;
return (
<InteractiveSearchContent
<InteractiveSearch
{...otherProps}
/>
);
}
}
InteractiveSearchContentConnector.propTypes = {
InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);

View File

@@ -4,7 +4,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import { align } from 'Helpers/Props';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import styles from './InteractiveSearchContent.css';
import styles from './InteractiveSearch.css';
function InteractiveSearchFilterMenu(props) {
const {

View File

@@ -1,23 +1,25 @@
.cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.protocol {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.titleContent {
display: flex;
align-items: center;
justify-content: space-between;
word-break: break-all;
}
.indexer {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 85px;
}
.quality,
.customFormat,
.languages {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.languages {
@@ -25,7 +27,7 @@
}
.customFormatScore {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
font-weight: bold;
@@ -33,31 +35,28 @@
}
.rejected,
.indexerFlags {
composes: cell;
.indexerFlags,
.download {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.age,
.size {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.peers {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}
.titleContent {
overflow-wrap: break-word;
}
.history {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}
@@ -67,7 +66,7 @@
}
.download {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}

View File

@@ -3,8 +3,6 @@
interface CssExports {
'age': string;
'blocklist': string;
'cell': string;
'customFormat': string;
'customFormatScore': string;
'download': string;
'downloadIcon': string;

View File

@@ -133,9 +133,9 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
longDateFormat,
timeFormat,
grabError,
historyGrabbedData,
historyFailedData,
blocklistData,
historyGrabbedData = {} as MovieHistory,
historyFailedData = {} as MovieHistory,
blocklistData = {} as MovieBlocklist,
searchPayload,
onGrabPress,
} = props;
@@ -199,53 +199,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
{formatAge(age, ageHours, ageMinutes)}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.RIGHT}
/>
) : null}
</TableRowCell>
<TableRowCell>
<div className={styles.titleContent}>
<Link to={infoUrl} title={title}>
@@ -316,10 +269,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
<MovieQuality quality={quality} />
</TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
@@ -348,6 +297,53 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
) : null}
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<ConfirmModal
isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

View File

@@ -1,11 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCastPoster extends Component {
@@ -60,7 +57,7 @@ class MovieCastPoster extends Component {
images,
posterWidth,
posterHeight,
importListId
importList
} = this.props;
const {
@@ -69,36 +66,31 @@ class MovieCastPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
{
importListId > 0 ?
<IconButton
className={styles.action}
name={icons.EDIT}
title={translate('EditPerson')}
onPress={this.onEditImportListPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div className={styles.controls}>
<MonitorToggleButton
className={styles.action}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<div
style={elementStyle}
@@ -148,12 +140,8 @@ MovieCastPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
importListId: PropTypes.number.isRequired,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
MovieCastPoster.defaultProps = {
importListId: 0
};
export default MovieCastPoster;

View File

@@ -1,11 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCrewPoster extends Component {
@@ -60,7 +57,7 @@ class MovieCrewPoster extends Component {
images,
posterWidth,
posterHeight,
importListId
importList
} = this.props;
const {
@@ -69,36 +66,31 @@ class MovieCrewPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
{
importListId > 0 ?
<IconButton
className={styles.action}
name={icons.EDIT}
title={translate('EditPerson')}
onPress={this.onEditImportListPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div className={styles.controls}>
<MonitorToggleButton
className={styles.action}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<div
style={elementStyle}
@@ -148,12 +140,8 @@ MovieCrewPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
importListId: PropTypes.number.isRequired,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
MovieCrewPoster.defaultProps = {
importListId: 0
};
export default MovieCrewPoster;

View File

@@ -18,7 +18,7 @@ function createMapStateToProps() {
}, []);
return {
items: crew
items: _.uniqBy(crew, 'personName')
};
}
);

View File

@@ -1,17 +1,13 @@
$hoverScale: 1.05;
.content {
border-radius: '5px';
transition: all 200ms ease-in;
&:hover {
z-index: 2;
box-shadow: 0 0 12px var(--black);
transition: all 200ms ease-in;
.controls {
opacity: 0.9;
transition: opacity 200ms linear 150ms;
}
}
}
@@ -50,22 +46,18 @@ $hoverScale: 1.05;
.controls {
position: absolute;
bottom: 10px;
left: 10px;
top: 10px;
z-index: 3;
border-radius: 4px;
background-color: #707070;
color: var(--white);
font-size: $smallFontSize;
opacity: 0;
transition: opacity 0;
}
.action {
composes: button from '~Components/Link/IconButton.css';
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 25px;
color: var(--white);
&:hover {
color: var(--radarrYellow);
color: var(--iconButtonHoverLightColor);
}
}

View File

@@ -1,11 +1,19 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
function createMapStateToProps() {
return createMovieCreditListSelector();
return createSelector(
createMovieCreditListSelector(),
(importList) => {
return {
importList
};
}
);
}
const mapDispatchToProps = {
@@ -20,7 +28,7 @@ class MovieCreditPosterConnector extends Component {
// Listeners
onImportListSelect = () => {
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', presetName: undefined });
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
};

View File

@@ -2,6 +2,10 @@
flex: 1 0 auto;
}
.movie {
padding: 10px;
}
.container {
padding: 10px;
}

View File

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

View File

@@ -1,12 +1,15 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import dimensions from 'Styles/Variables/dimensions';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
@@ -65,39 +68,11 @@ class MovieCreditPosters extends Component {
};
this._isInitialized = false;
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
if (this._grid &&
(prevState.width !== width ||
prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
@@ -117,7 +92,10 @@ class MovieCreditPosters extends Component {
});
};
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
//
// Render
render() {
const {
items,
itemComponent
@@ -126,99 +104,44 @@ class MovieCreditPosters extends Component {
const {
posterWidth,
posterHeight,
columnCount
} = this.state;
const movieIdx = rowIndex * columnCount + columnIndex;
const movie = items[movieIdx];
if (!movie) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={style}
>
<MovieCreditPosterConnector
key={movie.order}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={movie.personTmdbId}
personName={movie.personName}
job={movie.job}
character={movie.character}
images={movie.images}
/>
</div>
);
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
const rowCount = Math.ceil(items.length / columnCount);
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
<div className={styles.sliderContainer}>
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={3}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}
>
{items.map((credit) => (
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
<MovieCreditPosterConnector
key={credit.id}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
job={credit.job}
character={credit.character}
images={credit.images}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}
}

View File

@@ -1,3 +0,0 @@
.alternateTitle {
white-space: nowrap;
}

View File

@@ -1,28 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './MovieAlternateTitles.css';
function MovieAlternateTitles({ alternateTitles }) {
return (
<ul>
{
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
return (
<li
key={alternateTitle}
className={styles.alternateTitle}
>
{alternateTitle}
</li>
);
})
}
</ul>
);
}
MovieAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default MovieAlternateTitles;

View File

@@ -5,7 +5,7 @@
.header {
position: relative;
width: 100%;
height: 375px;
height: 425px;
}
.errorMessage {
@@ -39,10 +39,11 @@
}
.poster {
z-index: 2;
flex-shrink: 0;
margin-right: 35px;
width: 217px;
height: 319px;
width: 250px;
height: 368px;
}
.info {

View File

@@ -1,9 +1,9 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel';
@@ -23,12 +23,11 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -38,8 +37,6 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import formatRuntime from 'Utilities/Date/formatRuntime';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
@@ -57,14 +54,6 @@ function getFanartUrl(images) {
return _.find(images, { coverType: 'fanart' })?.url;
}
function getExpandedState(newState) {
return {
allExpanded: newState.allSelected,
allCollapsed: newState.allUnselected,
expandedState: newState.selectedState
};
}
class MovieDetails extends Component {
//
@@ -78,10 +67,8 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {},
selectedTabIndex: 0,
isInteractiveSearchModalOpen: false,
isMovieHistoryModalOpen: false,
overviewHeight: 0,
titleWidth: 0
};
@@ -114,10 +101,6 @@ class MovieDetails extends Component {
this.setState({ isOrganizeModalOpen: false });
};
onManageEpisodesPress = () => {
this.setState({ isManageEpisodesOpen: true });
};
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
};
@@ -134,6 +117,14 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false });
};
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
};
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditMovieModalOpen: false,
@@ -145,27 +136,12 @@ class MovieDetails extends Component {
this.setState({ isDeleteMovieModalOpen: false });
};
onExpandAllPress = () => {
const {
allExpanded,
expandedState
} = this.state;
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
onMovieHistoryPress = () => {
this.setState({ isMovieHistoryModalOpen: true });
};
onExpandPress = (seasonNumber, isExpanded) => {
this.setState((state) => {
const convertedState = {
allSelected: state.allExpanded,
allUnselected: state.allCollapsed,
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], seasonNumber, isExpanded, false);
return getExpandedState(newState);
});
onMovieHistoryModalClose = () => {
this.setState({ isMovieHistoryModalOpen: false });
};
onMeasure = ({ height }) => {
@@ -204,7 +180,12 @@ class MovieDetails extends Component {
if (
touchStart < 50 ||
this.props.isSidebarVisible ||
this.state.isEventModalOpen
this.state.isOrganizeModalOpen ||
this.state.isEditMovieModalOpen ||
this.state.isDeleteMovieModalOpen ||
this.state.isInteractiveImportModalOpen ||
this.state.isInteractiveSearchModalOpen ||
this.state.isMovieHistoryModalOpen
) {
return;
}
@@ -239,10 +220,6 @@ class MovieDetails extends Component {
}
};
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index });
};
//
// Render
@@ -295,9 +272,10 @@ class MovieDetails extends Component {
isEditMovieModalOpen,
isDeleteMovieModalOpen,
isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
isMovieHistoryModalOpen,
overviewHeight,
titleWidth,
selectedTabIndex
titleWidth
} = this.state;
const fanartUrl = getFanartUrl(images);
@@ -324,6 +302,14 @@ class MovieDetails extends Component {
onPress={onSearchPress}
/>
<PageToolbarButton
label={translate('InteractiveSearch')}
iconName={icons.INTERACTIVE}
isSpinning={isSearching}
title={undefined}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@@ -339,6 +325,12 @@ class MovieDetails extends Component {
onPress={this.onInteractiveImportPress}
/>
<PageToolbarButton
label={translate('History')}
iconName={icons.HISTORY}
onPress={this.onMovieHistoryPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@@ -654,101 +646,33 @@ class MovieDetails extends Component {
null
}
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
<TabList
className={styles.tabList}
>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('History')}
</Tab>
<FieldSet legend={translate('Files')}>
<MovieFileEditorTable
movieId={id}
/>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Search')}
</Tab>
<ExtraFileTable
movieId={id}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Files')}
</Tab>
<FieldSet legend={translate('Cast')}>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Titles')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Cast')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Crew')}
</Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector />
</div>
}
</TabList>
<TabPanel>
<MovieHistoryTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<InteractiveSearchTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieFileEditorTable
movieId={id}
/>
<ExtraFileTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieTitlesTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
<TabPanel>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
</Tabs>
<FieldSet legend={translate('Crew')}>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<FieldSet legend={translate('Titles')}>
<MovieTitlesTable
movieId={id}
/>
</FieldSet>
</div>
<OrganizePreviewModalConnector
@@ -764,6 +688,12 @@ class MovieDetails extends Component {
onDeleteMoviePress={this.onDeleteMoviePress}
/>
<MovieHistoryModal
isOpen={isMovieHistoryModalOpen}
movieId={id}
onModalClose={this.onMovieHistoryModalClose}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
movieId={id}
@@ -780,6 +710,12 @@ class MovieDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody>
</PageContent>
);

View File

@@ -11,7 +11,6 @@ import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
@@ -182,12 +181,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearMovieFiles() {
dispatch(clearMovieFiles());
},
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
dispatchFetchMovieCredits({ movieId }) {
dispatch(fetchMovieCredits({ movieId }));
},
@@ -283,7 +276,6 @@ class MovieDetailsConnector extends Component {
this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieBlocklist({ movieId });
this.props.dispatchFetchMovieHistory({ movieId });
this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId });
@@ -294,7 +286,6 @@ class MovieDetailsConnector extends Component {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieBlocklist();
this.props.dispatchClearMovieFiles();
this.props.dispatchClearMovieHistory();
this.props.dispatchClearExtraFiles();
this.props.dispatchClearMovieCredits();
this.props.dispatchClearQueueDetails();
@@ -351,8 +342,6 @@ MovieDetailsConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
dispatchFetchMovieFiles: PropTypes.func.isRequired,
dispatchClearMovieFiles: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired,
dispatchFetchExtraFiles: PropTypes.func.isRequired,
dispatchClearExtraFiles: PropTypes.func.isRequired,
dispatchFetchMovieCredits: PropTypes.func.isRequired,

View File

@@ -1,5 +1,4 @@
.container {
margin-top: 20px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);

View File

@@ -1,5 +1,6 @@
import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) {
const {
@@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
} = props;
return (
<MovieTitlesTableContentConnector
{...otherProps}
/>
<div className={styles.container}>
<MovieTitlesTableContentConnector
{...otherProps}
/>
</div>
);
}

View File

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

View File

@@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
function MovieHistoryModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
size={sizes.EXTRA_LARGE}
>
<MovieHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModal;

View File

@@ -0,0 +1,142 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
label: () => translate('Actions'),
isVisible: true
}
];
class MovieHistoryModalContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress,
onModalClose
} = this.props;
const hasItems = !!items.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('History')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
}
{
isPopulated && !hasItems && !error &&
<div>{translate('NoHistory')}</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
MovieHistoryModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModalContent;

View File

@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryTableContent from './MovieHistoryTableContent';
import { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryModalContent from './MovieHistoryModalContent';
function createMapStateToProps() {
return createSelector(
@@ -15,10 +15,29 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchMovieHistory,
clearMovieHistory,
movieHistoryMarkAsFailed
};
class MovieHistoryTableContentConnector extends Component {
class MovieHistoryModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
movieId
} = this.props;
this.props.fetchMovieHistory({
movieId
});
}
componentWillUnmount() {
this.props.clearMovieHistory();
}
//
// Listeners
@@ -39,7 +58,7 @@ class MovieHistoryTableContentConnector extends Component {
render() {
return (
<MovieHistoryTableContent
<MovieHistoryModalContent
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
@@ -47,9 +66,11 @@ class MovieHistoryTableContentConnector extends Component {
}
}
MovieHistoryTableContentConnector.propTypes = {
MovieHistoryModalContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
fetchMovieHistory: PropTypes.func.isRequired,
clearMovieHistory: PropTypes.func.isRequired,
movieHistoryMarkAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryTableContentConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);

View File

@@ -1,22 +0,0 @@
import React from 'react';
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
import styles from './MovieHistoryTable.css';
function MovieHistoryTable(props) {
const {
...otherProps
} = props;
return (
<div className={styles.container}>
<MovieHistoryTableContentConnector
{...otherProps}
/>
</div>
);
}
MovieHistoryTable.propTypes = {
};
export default MovieHistoryTable;

View File

@@ -1,128 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
import styles from './MovieHistoryTableContent.css';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: 'Custom format score'
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
isVisible: true
}
];
class MovieHistoryTableContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress
} = this.props;
const hasItems = !!items.length;
return (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>
{translate('UnableToLoadHistory')}
</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>
{translate('NoHistory')}
</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
MovieHistoryTableContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default MovieHistoryTableContent;

View File

@@ -26,7 +26,7 @@ import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { SelectStateInputProps } from 'typings/props';
import formatRuntime from 'Utilities/Date/formatRuntime';
import formatBytes from 'Utilities/Number/formatBytes';
import titleCase from 'Utilities/String/titleCase';
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import MovieIndexProgressBar from '../ProgressBar/MovieIndexProgressBar';
import MovieStatusCell from './MovieStatusCell';
@@ -286,7 +286,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
if (name === 'minimumAvailability') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{titleCase(minimumAvailability)}
{translate(firstCharToUpper(minimumAvailability))}
</VirtualTableRowCell>
);
}

View File

@@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieInteractiveSearchModalContent from './MovieInteractiveSearchModalContent';
function MovieInteractiveSearchModal(props) {
const {
isOpen,
movieId,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
size={sizes.EXTRA_EXTRA_LARGE}
>
<MovieInteractiveSearchModalContent
movieId={movieId}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModal;

View File

@@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import MovieInteractiveSearchModal from './MovieInteractiveSearchModal';
function createMapDispatchToProps(dispatch, props) {
return {
dispatchCancelFetchReleases() {
dispatch(cancelFetchReleases());
},
dispatchClearReleases() {
dispatch(clearReleases());
},
onModalClose() {
dispatch(cancelFetchReleases());
dispatch(clearReleases());
props.onModalClose();
}
};
}
class MovieInteractiveSearchModalConnector extends Component {
//
// Lifecycle
componentWillUnmount() {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearReleases();
}
//
// Render
render() {
const {
dispatchCancelFetchReleases,
dispatchClearReleases,
...otherProps
} = this.props;
return (
<MovieInteractiveSearchModal
{...otherProps}
/>
);
}
}
MovieInteractiveSearchModalConnector.propTypes = {
...MovieInteractiveSearchModal.propTypes,
dispatchCancelFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(MovieInteractiveSearchModalConnector);

View File

@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { scrollDirections } from 'Helpers/Props';
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
import translate from 'Utilities/String/translate';
function MovieInteractiveSearchModalContent(props) {
const {
movieId,
onModalClose
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('InteractiveSearchModalHeader')}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
<InteractiveSearchConnector
searchPayload={{ movieId }}
/>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
MovieInteractiveSearchModalContent.propTypes = {
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModalContent;

View File

@@ -1,5 +1,4 @@
.container {
margin-top: 20px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);

View File

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

View File

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

View File

@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER}
title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { name })}
message={translate('DeleteCustomFormatMessageText', { customFormatName: name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat}

View File

@@ -15,6 +15,7 @@ interface SavePayload {
enabled?: boolean;
enableAuto?: boolean;
qualityProfileId?: number;
minimumAvailability?: string;
rootFolderPath?: string;
}
@@ -58,6 +59,7 @@ function ManageImportListsEditModalContent(
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
NO_CHANGE
);
const [minimumAvailability, setMinimumAvailability] = useState(NO_CHANGE);
const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE);
const save = useCallback(() => {
@@ -79,6 +81,11 @@ function ManageImportListsEditModalContent(
payload.qualityProfileId = qualityProfileId as number;
}
if (minimumAvailability !== NO_CHANGE) {
hasChanges = true;
payload.minimumAvailability = minimumAvailability as string;
}
if (rootFolderPath !== NO_CHANGE) {
hasChanges = true;
payload.rootFolderPath = rootFolderPath;
@@ -93,6 +100,7 @@ function ManageImportListsEditModalContent(
enabled,
enableAuto,
qualityProfileId,
minimumAvailability,
rootFolderPath,
onSavePress,
onModalClose,
@@ -110,6 +118,9 @@ function ManageImportListsEditModalContent(
case 'qualityProfileId':
setQualityProfileId(value);
break;
case 'minimumAvailability':
setMinimumAvailability(value);
break;
case 'rootFolderPath':
setRootFolderPath(value);
break;
@@ -164,6 +175,19 @@ function ManageImportListsEditModalContent(
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
value={minimumAvailability}
includeNoChange={true}
includeNoChangeDisabled={false}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import ManageImportListsModalContent from './ManageImportListsModalContent';
interface ManageImportListsModalProps {
@@ -11,7 +12,7 @@ function ManageImportListsModal(props: ManageImportListsModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<Modal isOpen={isOpen} size={sizes.EXTRA_LARGE} onModalClose={onModalClose}>
<ManageImportListsModalContent onModalClose={onModalClose} />
</Modal>
);

View File

@@ -52,6 +52,12 @@ const COLUMNS = [
isSortable: true,
isVisible: true,
},
{
name: 'minimumAvailability',
label: () => translate('MinimumAvailability'),
isSortable: true,
isVisible: true,
},
{
name: 'rootFolderPath',
label: () => translate('RootFolder'),

View File

@@ -2,6 +2,7 @@
.tags,
.enabled,
.enableAuto,
.minimumAvailability,
.qualityProfileId,
.rootFolderPath,
.implementation {

View File

@@ -4,6 +4,7 @@ interface CssExports {
'enableAuto': string;
'enabled': string;
'implementation': string;
'minimumAvailability': string;
'name': string;
'qualityProfileId': string;
'rootFolderPath': 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 firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import styles from './ManageImportListsModalRow.css';
@@ -15,6 +16,7 @@ interface ManageImportListsModalRowProps {
name: string;
rootFolderPath: string;
qualityProfileId: number;
minimumAvailability: string;
implementation: string;
tags: number[];
enabled: boolean;
@@ -30,6 +32,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
isSelected,
name,
rootFolderPath,
minimumAvailability,
qualityProfileId,
implementation,
enabled,
@@ -69,6 +72,10 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
{qualityProfile?.name ?? translate('None')}
</TableRowCell>
<TableRowCell className={styles.minimumAvailability}>
{translate(firstCharToUpper(minimumAvailability))}
</TableRowCell>
<TableRowCell className={styles.rootFolderPath}>
{rootFolderPath}
</TableRowCell>

View File

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

View File

@@ -122,7 +122,7 @@ export default {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = payload.presetName ?? payload.implementationName;
selectedSchema.implementationName = payload.implementationName;
selectedSchema.minRefreshInterval = payload.minRefreshInterval;
selectedSchema.minRefreshInterval = selectedSchema.minRefreshInterval ?? payload.minRefreshInterval;
selectedSchema.minimumAvailability = 'released';
selectedSchema.rootFolderPath = '';

View File

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

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import moment from 'moment/moment';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
@@ -219,6 +220,42 @@ export const defaultState = {
const { ratings = {} } = item;
return ratings.tmdb? ratings.tmdb.value : 0;
},
inCinemas: function(item, direction) {
if (item.inCinemas) {
return moment(item.inCinemas).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
},
physicalRelease: function(item, direction) {
if (item.physicalRelease) {
return moment(item.physicalRelease).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
},
digitalRelease: function(item, direction) {
if (item.digitalRelease) {
return moment(item.digitalRelease).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
}
},

View File

@@ -91,7 +91,7 @@ export const defaultState = {
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, {
name: icons.SCORE,
title: 'Custom format score'
title: () => translate('CustomFormatScore')
}),
isVisible: false
},

View File

@@ -1,33 +0,0 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
function createMovieCollectionListSelector() {
return createSelector(
(state, { tmdbId }) => tmdbId,
(state) => state.settings.importLists.items,
(tmdbId, importLists) => {
const importListIds = _.reduce(importLists, (acc, list) => {
if (list.implementation === 'TMDbCollectionImport') {
const collectionIdField = list.fields.find((field) => {
return field.name === 'collectionId';
});
if (collectionIdField && parseInt(collectionIdField.value) === tmdbId) {
acc.push(list);
return acc;
}
}
return acc;
}, []);
if (importListIds.length === 0) {
return undefined;
}
return importListIds[0];
}
);
}
export default createMovieCollectionListSelector;

View File

@@ -21,15 +21,11 @@ function createMovieCreditListSelector() {
return acc;
}, []);
let importListId = 0;
if (importListIds.length > 0) {
importListId = importListIds[0].id;
if (importListIds.length === 0) {
return undefined;
}
return {
importListId
};
return importListIds[0];
}
);
}

View File

@@ -4,6 +4,8 @@ import { render } from 'react-dom';
import createAppStore from 'Store/createAppStore';
import App from './App/App';
import 'Diag/ConsoleApi';
export async function bootstrap() {
const history = createBrowserHistory();
const store = createAppStore(history);

View File

@@ -15,6 +15,7 @@ interface ImportList extends ModelBase {
enabled: boolean;
enableAuto: boolean;
qualityProfileId: number;
minimumAvailability: string;
rootFolderPath: string;
name: string;
fields: Field[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
}
}
return result;
return result.Where(t => t.IsNotNullOrWhiteSpace());
}
public override string Name => "Flood";

View File

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

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