mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a64826868 | |||
| cda40312e0 | |||
| 907779b4ce | |||
| cc03651af5 | |||
| 1ae98d618c | |||
| f5914da2f9 | |||
| f7816aa5cd | |||
| a652ce50a9 | |||
| 58b726a292 | |||
| 1d8cf6a7f5 | |||
| 2c3ad380ef | |||
| 0e7874aacf | |||
| 8638d82ad3 | |||
| f3d6a1f99d | |||
| fa036f5807 | |||
| a931f8a69f | |||
| a491c9a4a0 | |||
| 2aafb6369c | |||
| ef8253044e | |||
| c1feeb72ee | |||
| 21560cd6cc | |||
| bda2b9b0b8 | |||
| 4630de9616 | |||
| 7e83180e50 | |||
| e60eed49c7 | |||
| 74cfc94b4c | |||
| 213c55c7af | |||
| c066fa5e27 | |||
| 2741ecb968 | |||
| 7965c29425 | |||
| d2cbab70a9 | |||
| 16381a1aef | |||
| b92e08b850 | |||
| eab470c67f | |||
| 7f11659d95 | |||
| 03dec07cbe | |||
| 554c696ee6 | |||
| 093f8a39fe | |||
| 8a1663f136 | |||
| 251d2dde97 | |||
| 996542a4a5 | |||
| 0914d6250c | |||
| 3ff8e511b5 | |||
| 3a7b27fb45 | |||
| c81d2c97f5 | |||
| dae46524c4 | |||
| 3c6386f318 | |||
| 1400a8806d | |||
| e3f33f5a61 | |||
| e6f4b88cf3 | |||
| b788464487 | |||
| e29717ec6c | |||
| 5d7e23092f | |||
| 9921d51451 | |||
| 213620cb29 | |||
| bdc4aade0f | |||
| b2300dbf41 | |||
| 44289d30f9 | |||
| 260fb88f85 | |||
| 119cdf6f09 | |||
| c8d30fd214 | |||
| 7e9e528d3b | |||
| 8554c0d9cb | |||
| 22cc34b4fe | |||
| 990785ebfc | |||
| 957be99401 | |||
| 4bcde25e29 | |||
| 1d70f36e7d | |||
| cc0a448bc8 | |||
| c9e977baea | |||
| 6cb9a46cd4 | |||
| eef379277a | |||
| 41fef47684 | |||
| fcda6faf3d | |||
| 79bbf9c50b | |||
| 43d2f2804b | |||
| fa62f3f66a | |||
| 229d91fe40 | |||
| 2673d1eee4 | |||
| e59fd1118f | |||
| c1fd33b152 | |||
| 2f58c8676f | |||
| defc448304 | |||
| 3ec3358728 |
+2
-2
@@ -9,13 +9,13 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.2.4'
|
majorVersion: '5.3.1'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.413'
|
dotnetVersion: '6.0.417'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '16.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType) {
|
function getIconName(eventType, data) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@@ -17,7 +17,7 @@ function getIconName(eventType) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return icons.DELETE;
|
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('MovieDownloadFailedTooltip');
|
return translate('MovieDownloadFailedTooltip');
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return translate('MovieFileDeletedTooltip');
|
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return translate('MovieFileRenamedTooltip');
|
return translate('MovieFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
function HistoryEventTypeCell({ eventType, data }) {
|
||||||
const iconName = getIconName(eventType);
|
const iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
|
|||||||
@@ -85,8 +85,13 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.studio,
|
||||||
|
.genres {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
margin-left: 8px;
|
margin-left: 5px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface CssExports {
|
|||||||
'certification': string;
|
'certification': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
'exclusionIcon': string;
|
'exclusionIcon': string;
|
||||||
|
'genres': string;
|
||||||
'icons': string;
|
'icons': string;
|
||||||
'links': string;
|
'links': string;
|
||||||
'overlay': string;
|
'overlay': string;
|
||||||
@@ -14,6 +15,7 @@ interface CssExports {
|
|||||||
'runtime': string;
|
'runtime': string;
|
||||||
'searchResult': string;
|
'searchResult': string;
|
||||||
'statusContainer': string;
|
'statusContainer': string;
|
||||||
|
'studio': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
'titleContainer': string;
|
'titleContainer': string;
|
||||||
'titleRow': string;
|
'titleRow': string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
@@ -61,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
titleSlug,
|
titleSlug,
|
||||||
year,
|
year,
|
||||||
studio,
|
studio,
|
||||||
|
genres,
|
||||||
status,
|
status,
|
||||||
overview,
|
overview,
|
||||||
ratings,
|
ratings,
|
||||||
@@ -76,6 +78,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
hasFile,
|
hasFile,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
movieFile,
|
movieFile,
|
||||||
|
queueItem,
|
||||||
runtime,
|
runtime,
|
||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
certification
|
certification
|
||||||
@@ -197,13 +200,46 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
{
|
||||||
|
ratings.imdb ?
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<ImdbRating
|
||||||
|
ratings={ratings}
|
||||||
|
iconSize={13}
|
||||||
|
/>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!studio &&
|
!!studio &&
|
||||||
<Label size={sizes.LARGE}>
|
<Label size={sizes.LARGE}>
|
||||||
{studio}
|
<Icon
|
||||||
|
name={icons.STUDIO}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={styles.studio}>
|
||||||
|
{studio}
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
genres.length > 0 ?
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon
|
||||||
|
name={icons.GENRE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={styles.genres}>
|
||||||
|
{genres.slice(0, 3).join(', ')}
|
||||||
|
</span>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
anchor={
|
anchor={
|
||||||
<Label
|
<Label
|
||||||
@@ -215,15 +251,15 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
Links
|
{translate('Links')}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
<MovieDetailsLinks
|
<MovieDetailsLinks
|
||||||
tmdbId={tmdbId}
|
tmdbId={tmdbId}
|
||||||
youTubeTrailerId={youTubeTrailerId}
|
|
||||||
imdbId={imdbId}
|
imdbId={imdbId}
|
||||||
|
youTubeTrailerId={youTubeTrailerId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
canFlip={true}
|
canFlip={true}
|
||||||
@@ -237,6 +273,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
hasMovieFiles={hasFile}
|
hasMovieFiles={hasFile}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
|
queueItem={queueItem}
|
||||||
id={id}
|
id={id}
|
||||||
useLabel={true}
|
useLabel={true}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
@@ -273,6 +310,7 @@ AddNewMovieSearchResult.propTypes = {
|
|||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
studio: PropTypes.string,
|
studio: PropTypes.string,
|
||||||
|
genres: PropTypes.arrayOf(PropTypes.string),
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
@@ -283,15 +321,19 @@ AddNewMovieSearchResult.propTypes = {
|
|||||||
isExclusionMovie: PropTypes.bool.isRequired,
|
isExclusionMovie: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
queueItems: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
hasFile: PropTypes.bool.isRequired,
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
movieFile: PropTypes.object,
|
movieFile: PropTypes.object,
|
||||||
|
queueItem: PropTypes.object,
|
||||||
colorImpairedMode: PropTypes.bool,
|
colorImpairedMode: PropTypes.bool,
|
||||||
runtime: PropTypes.number.isRequired,
|
runtime: PropTypes.number.isRequired,
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||||
certification: PropTypes.string
|
certification: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AddNewMovieSearchResult.defaultProps = {
|
||||||
|
genres: []
|
||||||
|
};
|
||||||
|
|
||||||
export default AddNewMovieSearchResult;
|
export default AddNewMovieSearchResult;
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ function createMapStateToProps() {
|
|||||||
createExistingMovieSelector(),
|
createExistingMovieSelector(),
|
||||||
createExclusionMovieSelector(),
|
createExclusionMovieSelector(),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
(state) => state.queue.details.items,
|
||||||
(state, { internalId }) => internalId,
|
(state, { internalId }) => internalId,
|
||||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||||
(isExistingMovie, isExclusionMovie, dimensions, internalId, movieRuntimeFormat) => {
|
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId, movieRuntimeFormat) => {
|
||||||
|
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
existingMovieId: internalId,
|
existingMovieId: internalId,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isExclusionMovie,
|
isExclusionMovie,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
queueItem,
|
||||||
movieRuntimeFormat
|
movieRuntimeFormat
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
.contentContainer {
|
.contentContainer {
|
||||||
z-index: $popperZIndex;
|
z-index: $popperZIndex;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
/* 400px container witdh with 8px padding on each side */
|
/* 400px container width with 8px padding on each side */
|
||||||
width: 384px;
|
width: 384px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
|
|||||||
className={styles.addErrorAlert}
|
className={styles.addErrorAlert}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{translate('UnableToAddRootFolder')}
|
{translate('AddRootFolderError')}
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,16 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
|
|||||||
gotoCalendarToday
|
gotoCalendarToday
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||||
|
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
fetchCalendar();
|
fetchCalendar();
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class SignalRConnector extends Component {
|
|||||||
const resource = body.resource;
|
const resource = body.resource;
|
||||||
const status = resource.status;
|
const status = resource.status;
|
||||||
|
|
||||||
// Both sucessful and failed commands need to be
|
// Both successful and failed commands need to be
|
||||||
// completed, otherwise they spin until they timeout.
|
// completed, otherwise they spin until they timeout.
|
||||||
|
|
||||||
if (status === 'completed' || status === 'failed') {
|
if (status === 'completed' || status === 'failed') {
|
||||||
@@ -187,6 +187,8 @@ class SignalRConnector extends Component {
|
|||||||
repopulatePage('movieFileUpdated');
|
repopulatePage('movieFileUpdated');
|
||||||
} else if (body.action === 'deleted') {
|
} else if (body.action === 'deleted') {
|
||||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||||
|
|
||||||
|
repopulatePage('movieFileDeleted');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
"start_url": "../../../../",
|
"start_url": "../../../../",
|
||||||
"theme_color": "#3a3f51",
|
"theme_color": "#3a3f51",
|
||||||
"background_color": "#3a3f51",
|
"background_color": "#3a3f51",
|
||||||
"display": "standalone"
|
"display": "minimal-ui"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
faEye as fasEye,
|
faEye as fasEye,
|
||||||
faFastBackward as fasFastBackward,
|
faFastBackward as fasFastBackward,
|
||||||
faFastForward as fasFastForward,
|
faFastForward as fasFastForward,
|
||||||
|
faFileCircleQuestion as fasFileCircleQuestion,
|
||||||
faFileExport as fasFileExport,
|
faFileExport as fasFileExport,
|
||||||
faFileInvoice as farFileInvoice,
|
faFileInvoice as farFileInvoice,
|
||||||
faFilm as fasFilm,
|
faFilm as fasFilm,
|
||||||
@@ -159,6 +160,7 @@ export const EXPORT = fasFileExport;
|
|||||||
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
||||||
export const FATAL = fasTimesCircle;
|
export const FATAL = fasTimesCircle;
|
||||||
export const FILE = farFile;
|
export const FILE = farFile;
|
||||||
|
export const FILE_MISSING = fasFileCircleQuestion;
|
||||||
export const FILM = fasFilm;
|
export const FILM = fasFilm;
|
||||||
export const FILTER = fasFilter;
|
export const FILTER = fasFilter;
|
||||||
export const FLAG = fasFlag;
|
export const FLAG = fasFlag;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.languages {
|
.languages {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.quality}>
|
<TableRowCell className={styles.quality}>
|
||||||
<MovieQuality quality={quality} />
|
<MovieQuality quality={quality} showRevision={true} />
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
|
|||||||
@@ -5,6 +5,29 @@ import { createSelector } from 'reselect';
|
|||||||
import MovieCreditPosters from '../MovieCreditPosters';
|
import MovieCreditPosters from '../MovieCreditPosters';
|
||||||
import MovieCrewPoster from './MovieCrewPoster';
|
import MovieCrewPoster from './MovieCrewPoster';
|
||||||
|
|
||||||
|
function crewSort(a, b) {
|
||||||
|
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
|
||||||
|
|
||||||
|
const indexA = jobOrder.indexOf(a.job);
|
||||||
|
const indexB = jobOrder.indexOf(b.job);
|
||||||
|
|
||||||
|
if (indexA === -1 && indexB === -1) {
|
||||||
|
return 0;
|
||||||
|
} else if (indexA === -1) {
|
||||||
|
return 1;
|
||||||
|
} else if (indexB === -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexA < indexB) {
|
||||||
|
return -1;
|
||||||
|
} else if (indexA > indexB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.movieCredits.items,
|
(state) => state.movieCredits.items,
|
||||||
@@ -17,8 +40,10 @@ function createMapStateToProps() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const sortedCrew = crew.sort(crewSort);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: _.uniqBy(crew, 'personName')
|
items: _.uniqBy(sortedCrew, 'personName')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,3 +9,9 @@
|
|||||||
.container {
|
.container {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sliderContainer {
|
||||||
|
--swiper-navigation-color: var(--white);
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface CssExports {
|
|||||||
'container': string;
|
'container': string;
|
||||||
'grid': string;
|
'grid': string;
|
||||||
'movie': string;
|
'movie': string;
|
||||||
|
'sliderContainer': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -14,24 +14,6 @@ import 'swiper/css/navigation';
|
|||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
||||||
const additionalColumnCount = {
|
|
||||||
small: 3,
|
|
||||||
medium: 2,
|
|
||||||
large: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
|
||||||
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
|
|
||||||
const columns = Math.floor(width / maxiumColumnWidth);
|
|
||||||
const remainder = width % maxiumColumnWidth;
|
|
||||||
|
|
||||||
if (remainder === 0 && posterSize === 'large') {
|
|
||||||
return maxiumColumnWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateRowHeight(posterHeight, isSmallScreen) {
|
function calculateRowHeight(posterHeight, isSmallScreen) {
|
||||||
const titleHeight = 19;
|
const titleHeight = 19;
|
||||||
const characterHeight = 19;
|
const characterHeight = 19;
|
||||||
@@ -46,10 +28,6 @@ function calculateRowHeight(posterHeight, isSmallScreen) {
|
|||||||
return heights.reduce((acc, height) => acc + height, 0);
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePosterHeight(posterWidth) {
|
|
||||||
return Math.ceil((250 / 170) * posterWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieCreditPosters extends Component {
|
class MovieCreditPosters extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -66,39 +44,16 @@ class MovieCreditPosters extends Component {
|
|||||||
posterHeight: 238,
|
posterHeight: 238,
|
||||||
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
||||||
};
|
};
|
||||||
|
|
||||||
this._isInitialized = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
|
||||||
|
|
||||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
|
||||||
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
|
|
||||||
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
|
||||||
const posterWidth = columnWidth - padding;
|
|
||||||
const posterHeight = calculatePosterHeight(posterWidth);
|
|
||||||
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
width,
|
|
||||||
columnWidth,
|
|
||||||
columnCount,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
rowHeight
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
itemComponent
|
itemComponent,
|
||||||
|
isSmallScreen
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -113,14 +68,13 @@ class MovieCreditPosters extends Component {
|
|||||||
<Swiper
|
<Swiper
|
||||||
slidesPerView='auto'
|
slidesPerView='auto'
|
||||||
spaceBetween={10}
|
spaceBetween={10}
|
||||||
slidesPerGroup={3}
|
slidesPerGroup={isSmallScreen ? 1 : 3}
|
||||||
|
navigation={true}
|
||||||
loop={false}
|
loop={false}
|
||||||
loopFillGroupWithBlank={true}
|
loopFillGroupWithBlank={true}
|
||||||
className="mySwiper"
|
className="mySwiper"
|
||||||
modules={[Navigation]}
|
modules={[Navigation]}
|
||||||
onInit={(swiper) => {
|
onInit={(swiper) => {
|
||||||
swiper.params.navigation.prevEl = this._swiperPrevRef;
|
|
||||||
swiper.params.navigation.nextEl = this._swiperNextRef;
|
|
||||||
swiper.navigation.init();
|
swiper.navigation.init();
|
||||||
swiper.navigation.update();
|
swiper.navigation.update();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -320,8 +320,8 @@ class MovieDetails extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('ManualImport')}
|
label={translate('ManageFiles')}
|
||||||
iconName={icons.INTERACTIVE}
|
iconName={icons.MOVIE_FILE}
|
||||||
onPress={this.onInteractiveImportPress}
|
onPress={this.onInteractiveImportPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -704,6 +704,7 @@ class MovieDetails extends Component {
|
|||||||
<InteractiveImportModal
|
<InteractiveImportModal
|
||||||
isOpen={isInteractiveImportModalOpen}
|
isOpen={isInteractiveImportModalOpen}
|
||||||
movieId={id}
|
movieId={id}
|
||||||
|
modalTitle={translate('ManageFiles')}
|
||||||
folder={path}
|
folder={path}
|
||||||
allowMovieChange={false}
|
allowMovieChange={false}
|
||||||
showFilterExistingFiles={true}
|
showFilterExistingFiles={true}
|
||||||
|
|||||||
@@ -33,14 +33,11 @@ const selectMovieFiles = createSelector(
|
|||||||
|
|
||||||
const hasMovieFiles = !!items.length;
|
const hasMovieFiles = !!items.length;
|
||||||
|
|
||||||
const sizeOnDisk = items.map((item) => item.size).reduce((prev, curr) => prev + curr, 0);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMovieFilesFetching: isFetching,
|
isMovieFilesFetching: isFetching,
|
||||||
isMovieFilesPopulated: isPopulated,
|
isMovieFilesPopulated: isPopulated,
|
||||||
movieFilesError: error,
|
movieFilesError: error,
|
||||||
hasMovieFiles,
|
hasMovieFiles
|
||||||
sizeOnDisk
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -104,8 +101,7 @@ function createMapStateToProps() {
|
|||||||
isMovieFilesFetching,
|
isMovieFilesFetching,
|
||||||
isMovieFilesPopulated,
|
isMovieFilesPopulated,
|
||||||
movieFilesError,
|
movieFilesError,
|
||||||
hasMovieFiles,
|
hasMovieFiles
|
||||||
sizeOnDisk
|
|
||||||
} = movieFiles;
|
} = movieFiles;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -161,7 +157,6 @@ function createMapStateToProps() {
|
|||||||
movieCreditsError,
|
movieCreditsError,
|
||||||
extraFilesError,
|
extraFilesError,
|
||||||
hasMovieFiles,
|
hasMovieFiles,
|
||||||
sizeOnDisk,
|
|
||||||
previousMovie,
|
previousMovie,
|
||||||
nextMovie,
|
nextMovie,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
|||||||
@@ -6,31 +6,43 @@ import MovieTitlesTableContent from './MovieTitlesTableContent';
|
|||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
(state, { movieId }) => movieId,
|
||||||
(state) => state.movies,
|
(state) => state.movies,
|
||||||
(movies) => {
|
(movieId, movies) => {
|
||||||
return movies;
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items
|
||||||
|
} = movies;
|
||||||
|
|
||||||
|
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
alternateTitles
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
// fetchMovies
|
|
||||||
};
|
|
||||||
|
|
||||||
class MovieTitlesTableContentConnector extends Component {
|
class MovieTitlesTableContentConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const movie = this.props.items.filter((obj) => {
|
const {
|
||||||
return obj.id === this.props.movieId;
|
alternateTitles,
|
||||||
});
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieTitlesTableContent
|
<MovieTitlesTableContent
|
||||||
{...this.props}
|
{...otherProps}
|
||||||
items={movie[0].alternateTitles}
|
items={alternateTitles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -38,7 +50,11 @@ class MovieTitlesTableContentConnector extends Component {
|
|||||||
|
|
||||||
MovieTitlesTableContentConnector.propTypes = {
|
MovieTitlesTableContentConnector.propTypes = {
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
|
MovieTitlesTableContentConnector.defaultProps = {
|
||||||
|
alternateTitles: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
label: () => translate('Actions'),
|
|
||||||
isVisible: true
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
@@ -103,20 +102,11 @@ class MovieHistoryRow extends Component {
|
|||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
<MovieFormats
|
<MovieFormats formats={customFormats} />
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
<Tooltip
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<MovieFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.TOP}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
@@ -134,6 +124,7 @@ class MovieHistoryRow extends Component {
|
|||||||
<IconButton
|
<IconButton
|
||||||
title={translate('MarkAsFailed')}
|
title={translate('MarkAsFailed')}
|
||||||
name={icons.REMOVE}
|
name={icons.REMOVE}
|
||||||
|
size={14}
|
||||||
onPress={this.onMarkAsFailedPress}
|
onPress={this.onMarkAsFailedPress}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
||||||
import { MOVIE_SEARCH } from 'Commands/commandNames';
|
import { MOVIE_SEARCH } from 'Commands/commandNames';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||||
@@ -21,11 +22,12 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
|||||||
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
|
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
totalItems,
|
|
||||||
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
|
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
|
||||||
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
|
||||||
|
|
||||||
const { isSelectMode, selectedFilterKey } = props;
|
const { isSelectMode, selectedFilterKey } = props;
|
||||||
const [selectState] = useSelect();
|
const [selectState] = useSelect();
|
||||||
const { selectedState } = selectState;
|
const { selectedState } = selectState;
|
||||||
@@ -50,6 +52,8 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
|||||||
: translate('SearchAll');
|
: translate('SearchAll');
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
|
setIsConfirmModalOpen(false);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
executeCommand({
|
executeCommand({
|
||||||
name: MOVIE_SEARCH,
|
name: MOVIE_SEARCH,
|
||||||
@@ -58,14 +62,36 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
|||||||
);
|
);
|
||||||
}, [dispatch, moviesToSearch]);
|
}, [dispatch, moviesToSearch]);
|
||||||
|
|
||||||
|
const onConfirmPress = useCallback(() => {
|
||||||
|
setIsConfirmModalOpen(true);
|
||||||
|
}, [setIsConfirmModalOpen]);
|
||||||
|
|
||||||
|
const onConfirmModalClose = useCallback(() => {
|
||||||
|
setIsConfirmModalOpen(false);
|
||||||
|
}, [setIsConfirmModalOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageToolbarButton
|
<>
|
||||||
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
<PageToolbarButton
|
||||||
isSpinning={isSearching}
|
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||||
isDisabled={!totalItems}
|
isSpinning={isSearching}
|
||||||
iconName={icons.SEARCH}
|
isDisabled={!items.length}
|
||||||
onPress={onPress}
|
iconName={icons.SEARCH}
|
||||||
/>
|
onPress={moviesToSearch.length > 5 ? onConfirmPress : onPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||||
|
message={translate('SearchMoviesConfirmationMessageText', {
|
||||||
|
count: moviesToSearch.length,
|
||||||
|
})}
|
||||||
|
confirmLabel={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||||
|
onConfirm={onPress}
|
||||||
|
onCancel={onConfirmModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -256,13 +256,18 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
if (current) {
|
if (current) {
|
||||||
const width = current.clientWidth;
|
const width = current.clientWidth;
|
||||||
const padding = bodyPadding - 5;
|
const padding = bodyPadding - 5;
|
||||||
|
const finalWidth = width - padding * 2;
|
||||||
|
|
||||||
|
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSize({
|
setSize({
|
||||||
width: width - padding * 2,
|
width: finalWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isSmallScreen, scrollerRef, bounds]);
|
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||||
const revision = quality.revision;
|
const revision = quality.revision;
|
||||||
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
|||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function revisionLabel(className, quality, showRevision) {
|
||||||
|
if (!showRevision) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.isRepack) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Repack')}
|
||||||
|
>
|
||||||
|
R
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality.revision.version && quality.revision.version > 1) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
title={translate('Proper')}
|
||||||
|
>
|
||||||
|
P
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function MovieQuality(props) {
|
function MovieQuality(props) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
@@ -35,7 +66,8 @@ function MovieQuality(props) {
|
|||||||
quality,
|
quality,
|
||||||
size,
|
size,
|
||||||
isMonitored,
|
isMonitored,
|
||||||
isCutoffNotMet
|
isCutoffNotMet,
|
||||||
|
showRevision
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let kind = kinds.DEFAULT;
|
let kind = kinds.DEFAULT;
|
||||||
@@ -50,13 +82,15 @@ function MovieQuality(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<span>
|
||||||
className={className}
|
<Label
|
||||||
kind={kind}
|
className={className}
|
||||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
kind={kind}
|
||||||
>
|
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||||
{quality.quality.name}
|
>
|
||||||
</Label>
|
{quality.quality.name}
|
||||||
|
</Label>{revisionLabel(className, quality, showRevision)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +100,14 @@ MovieQuality.propTypes = {
|
|||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
isMonitored: PropTypes.bool,
|
isMonitored: PropTypes.bool,
|
||||||
isCutoffNotMet: PropTypes.bool
|
isCutoffNotMet: PropTypes.bool,
|
||||||
|
showRevision: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
MovieQuality.defaultProps = {
|
MovieQuality.defaultProps = {
|
||||||
title: '',
|
title: '',
|
||||||
isMonitored: true
|
isMonitored: true,
|
||||||
|
showRevision: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MovieQuality;
|
export default MovieQuality;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -49,7 +49,7 @@ function RootFolders() {
|
|||||||
|
|
||||||
if (!isFetching && !!error) {
|
if (!isFetching && !!error) {
|
||||||
return (
|
return (
|
||||||
<Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert>
|
<Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
|
|||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteDownloadClients,
|
bulkDeleteDownloadClients,
|
||||||
bulkEditDownloadClients,
|
bulkEditDownloadClients,
|
||||||
|
setManageDownloadClientsSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
@@ -80,6 +82,8 @@ const COLUMNS = [
|
|||||||
|
|
||||||
interface ManageDownloadClientsModalContentProps {
|
interface ManageDownloadClientsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageDownloadClientsModalContent(
|
function ManageDownloadClientsModalContent(
|
||||||
@@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
|
|||||||
isSaving,
|
isSaving,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
}: DownloadClientAppState = useSelector(
|
}: DownloadClientAppState = useSelector(
|
||||||
createClientSideCollectionSelector('settings.downloadClients')
|
createClientSideCollectionSelector('settings.downloadClients')
|
||||||
);
|
);
|
||||||
@@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
|
|||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
dispatch(setManageDownloadClientsSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const onDeletePress = useCallback(() => {
|
const onDeletePress = useCallback(() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}, [setIsDeleteModalOpen]);
|
}, [setIsDeleteModalOpen]);
|
||||||
@@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
|
|||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
|
|||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import {
|
import {
|
||||||
bulkDeleteIndexers,
|
bulkDeleteIndexers,
|
||||||
bulkEditIndexers,
|
bulkEditIndexers,
|
||||||
|
setManageIndexersSort,
|
||||||
} from 'Store/Actions/settingsActions';
|
} from 'Store/Actions/settingsActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
@@ -80,6 +82,8 @@ const COLUMNS = [
|
|||||||
|
|
||||||
interface ManageIndexersModalContentProps {
|
interface ManageIndexersModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
|
sortKey?: string;
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||||||
isSaving,
|
isSaving,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
}: IndexerAppState = useSelector(
|
}: IndexerAppState = useSelector(
|
||||||
createClientSideCollectionSelector('settings.indexers')
|
createClientSideCollectionSelector('settings.indexers')
|
||||||
);
|
);
|
||||||
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onSortPress = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
dispatch(setManageIndexersSort({ sortKey: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const onDeletePress = useCallback(() => {
|
const onDeletePress = useCallback(() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}, [setIsDeleteModalOpen]);
|
}, [setIsDeleteModalOpen]);
|
||||||
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
|||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
onSelectAllChange={onSelectAllChange}
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AddRootFolder.css';
|
|
||||||
|
|
||||||
class AddRootFolder extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddNewRootFolderModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
onAddNewRootFolderPress = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onNewRootFolderSelect = ({ value }) => {
|
|
||||||
this.props.onNewRootFolderSelect(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddRootFolderModalClose = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.addRootFolderButtonContainer}>
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
onPress={this.onAddNewRootFolderPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.importButtonIcon}
|
|
||||||
name={icons.DRIVE}
|
|
||||||
/>
|
|
||||||
{translate('AddRootFolder')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<FileBrowserModal
|
|
||||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
|
||||||
name="rootFolderPath"
|
|
||||||
value=""
|
|
||||||
onChange={this.onNewRootFolderSelect}
|
|
||||||
onModalClose={this.onAddRootFolderModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddRootFolder.propTypes = {
|
|
||||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddRootFolder;
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||||
|
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AddRootFolder.css';
|
import styles from './AddRootFolder.css';
|
||||||
|
|
||||||
function AddRootFolder() {
|
function AddRootFolder() {
|
||||||
|
const { isSaving, saveError } = useSelector(createRootFoldersSelector());
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
|
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
|
||||||
@@ -30,24 +34,42 @@ function AddRootFolder() {
|
|||||||
}, [setIsAddNewRootFolderModalOpen]);
|
}, [setIsAddNewRootFolderModalOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.addRootFolderButtonContainer}>
|
<>
|
||||||
<Button
|
{!isSaving && saveError ? (
|
||||||
kind={kinds.PRIMARY}
|
<Alert kind={kinds.DANGER}>
|
||||||
size={sizes.LARGE}
|
{translate('AddRootFolderError')}
|
||||||
onPress={onAddNewRootFolderPress}
|
|
||||||
>
|
|
||||||
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
|
|
||||||
{translate('AddRootFolder')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<FileBrowserModal
|
<ul>
|
||||||
isOpen={isAddNewRootFolderModalOpen}
|
{Array.isArray(saveError.responseJSON) ? (
|
||||||
name="rootFolderPath"
|
saveError.responseJSON.map((e, index) => {
|
||||||
value=""
|
return <li key={index}>{e.errorMessage}</li>;
|
||||||
onChange={onNewRootFolderSelect}
|
})
|
||||||
onModalClose={onAddRootFolderModalClose}
|
) : (
|
||||||
/>
|
<li>{JSON.stringify(saveError.responseJSON)}</li>
|
||||||
</div>
|
)}
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className={styles.addRootFolderButtonContainer}>
|
||||||
|
<Button
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
onPress={onAddNewRootFolderPress}
|
||||||
|
>
|
||||||
|
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
|
||||||
|
{translate('AddRootFolder')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<FileBrowserModal
|
||||||
|
isOpen={isAddNewRootFolderModalOpen}
|
||||||
|
name="rootFolderPath"
|
||||||
|
value=""
|
||||||
|
onChange={onNewRootFolderSelect}
|
||||||
|
onModalClose={onAddRootFolderModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
|||||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||||
|
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
@@ -31,9 +33,9 @@ export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadCl
|
|||||||
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
|
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
|
||||||
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
|
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
|
||||||
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
||||||
|
|
||||||
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
|
||||||
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
||||||
|
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
||||||
|
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
@@ -48,9 +50,9 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
|
|||||||
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
|
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
|
||||||
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
|
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
|
||||||
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
||||||
|
|
||||||
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
|
||||||
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
||||||
|
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
||||||
|
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
|
||||||
|
|
||||||
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
@@ -90,7 +92,9 @@ export default {
|
|||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.DESCENDING
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -124,7 +128,10 @@ export default {
|
|||||||
|
|
||||||
return selectedSchema;
|
return selectedSchema;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
|||||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||||
|
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import { createThunk } from 'Store/thunks';
|
||||||
@@ -35,9 +37,9 @@ export const DELETE_INDEXER = 'settings/indexers/deleteIndexer';
|
|||||||
export const TEST_INDEXER = 'settings/indexers/testIndexer';
|
export const TEST_INDEXER = 'settings/indexers/testIndexer';
|
||||||
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
||||||
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
||||||
|
|
||||||
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
|
||||||
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
||||||
|
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
||||||
|
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
@@ -53,9 +55,9 @@ export const deleteIndexer = createThunk(DELETE_INDEXER);
|
|||||||
export const testIndexer = createThunk(TEST_INDEXER);
|
export const testIndexer = createThunk(TEST_INDEXER);
|
||||||
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
||||||
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
||||||
|
|
||||||
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
|
||||||
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
||||||
|
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
||||||
|
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
|
||||||
|
|
||||||
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
@@ -95,7 +97,9 @@ export default {
|
|||||||
isTesting: false,
|
isTesting: false,
|
||||||
isTestingAll: false,
|
isTestingAll: false,
|
||||||
items: [],
|
items: [],
|
||||||
pendingChanges: {}
|
pendingChanges: {},
|
||||||
|
sortKey: 'name',
|
||||||
|
sortDirection: sortDirections.DESCENDING
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -157,7 +161,10 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
return updateSectionState(state, section, newState);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -130,7 +130,10 @@ export const actionHandlers = handleThunks({
|
|||||||
|
|
||||||
promise.done((data) => {
|
promise.done((data) => {
|
||||||
const updatedItem = _.cloneDeep(data);
|
const updatedItem = _.cloneDeep(data);
|
||||||
|
updatedItem.internalId = updatedItem.id;
|
||||||
updatedItem.id = updatedItem.tmdbId;
|
updatedItem.id = updatedItem.tmdbId;
|
||||||
|
delete updatedItem.images;
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
updateItem({ section: 'movies', ...data }),
|
updateItem({ section: 'movies', ...data }),
|
||||||
updateItem({ section: 'addMovie', ...updatedItem }),
|
updateItem({ section: 'addMovie', ...updatedItem }),
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const defaultState = {
|
|||||||
{
|
{
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
label: () => translate('Tags'),
|
label: () => translate('Tags'),
|
||||||
isSortable: false,
|
isSortable: true,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
function createDimensionsSelector() {
|
function createDimensionsSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.app.dimensions,
|
(state: AppState) => state.app.dimensions,
|
||||||
(dimensions) => {
|
(dimensions) => {
|
||||||
return dimensions;
|
return dimensions;
|
||||||
}
|
}
|
||||||
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@juggle/resize-observer": "3.4.0",
|
"@juggle/resize-observer": "3.4.0",
|
||||||
"@microsoft/signalr": "6.0.21",
|
"@microsoft/signalr": "6.0.25",
|
||||||
"@sentry/browser": "7.51.2",
|
"@sentry/browser": "7.51.2",
|
||||||
"@sentry/integrations": "7.51.2",
|
"@sentry/integrations": "7.51.2",
|
||||||
"@types/node": "18.16.8",
|
"@types/node": "18.16.8",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
using Radarr.Http.ClientSchema;
|
using Radarr.Http.ClientSchema;
|
||||||
|
|
||||||
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class SchemaBuilderFixture : TestBase
|
public class SchemaBuilderFixture : TestBase
|
||||||
{
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<ILocalizationService>()
|
||||||
|
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
|
||||||
|
.Returns<string, Dictionary<string, object>>((s, d) => s);
|
||||||
|
|
||||||
|
SchemaBuilder.Initialize(Mocker.Container);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_field_for_every_property()
|
public void should_return_field_for_every_property()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
// Use mirrors for tests that use two hosts
|
// Use mirrors for tests that use two hosts
|
||||||
var candidates = new[] { "httpbin1.servarr.com" };
|
var candidates = new[] { "httpbin1.servarr.com" };
|
||||||
|
|
||||||
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
|
// httpbin.org is broken right now, occasionally redirecting to https if it's unavailable.
|
||||||
_httpBinHost = mainHost;
|
_httpBinHost = mainHost;
|
||||||
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
|
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Warn(ex, "Coudn't set app folder permission");
|
_logger.Warn(ex, "Couldn't set app folder permission");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
|
|||||||
{
|
{
|
||||||
data = new XElement("base64", Convert.ToBase64String(bytes));
|
data = new XElement("base64", Convert.ToBase64String(bytes));
|
||||||
}
|
}
|
||||||
|
else if (value is Dictionary<string, string> d)
|
||||||
|
{
|
||||||
|
data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value))));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
_debounce = new SentryDebounce();
|
_debounce = new SentryDebounce();
|
||||||
|
|
||||||
// initialize to true and reconfigure later
|
// initialize to true and reconfigure later
|
||||||
// Otherwise it will default to false and any errors occuring
|
// Otherwise it will default to false and any errors occurring
|
||||||
// before config file gets read will not be filtered
|
// before config file gets read will not be filtered
|
||||||
FilterEvents = true;
|
FilterEvents = true;
|
||||||
SentryEnabled = true;
|
SentryEnabled = true;
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ namespace NzbDrone.Common.OAuth
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a request elements concatentation value to send with a request.
|
/// Creates a request elements concatenation value to send with a request.
|
||||||
/// This is also known as the signature base.
|
/// This is also known as the signature base.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso href="http://oauth.net/core/1.0#rfc.section.9.1.3"/>
|
/// <seealso href="http://oauth.net/core/1.0#rfc.section.9.1.3"/>
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
|
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="5.2.3" />
|
<PackageReference Include="NLog" Version="5.2.3" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3" />
|
||||||
<PackageReference Include="Npgsql" Version="7.0.4" />
|
<PackageReference Include="Npgsql" Version="7.0.6" />
|
||||||
<PackageReference Include="Sentry" Version="3.23.1" />
|
<PackageReference Include="Sentry" Version="3.23.1" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.8" />
|
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ namespace NzbDrone.Common
|
|||||||
|
|
||||||
if (dacls.Contains(authenticatedUsersDacl))
|
if (dacls.Contains(authenticatedUsersDacl))
|
||||||
{
|
{
|
||||||
// Permssions already set
|
// Permissions already set
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -4,6 +4,7 @@ using NUnit.Framework;
|
|||||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||||
using NzbDrone.Core.ImportLists;
|
using NzbDrone.Core.ImportLists;
|
||||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||||
@@ -42,8 +43,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
|||||||
{
|
{
|
||||||
GivenImportList();
|
GivenImportList();
|
||||||
|
|
||||||
|
var movieMetadata = Builder<MovieMetadata>.CreateNew().BuildNew();
|
||||||
|
|
||||||
|
Db.Insert(movieMetadata);
|
||||||
|
|
||||||
var status = Builder<ImportListMovie>.CreateNew()
|
var status = Builder<ImportListMovie>.CreateNew()
|
||||||
.With(h => h.ListId = _importList.Id)
|
.With(h => h.ListId = _importList.Id)
|
||||||
|
.With(b => b.MovieMetadataId = movieMetadata.Id)
|
||||||
.BuildNew();
|
.BuildNew();
|
||||||
Db.Insert(status);
|
Db.Insert(status);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NLog;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.IndexerTests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class IndexerBaseFixture : CoreTest<IndexerBase<TestIndexerSettings>>
|
||||||
|
{
|
||||||
|
private TestIndexer _indexer;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_indexer = new TestIndexer(new Mock<IHttpClient>().Object,
|
||||||
|
new Mock<IIndexerStatusService>().Object,
|
||||||
|
new Mock<IConfigService>().Object,
|
||||||
|
new Mock<IParsingService>().Object,
|
||||||
|
new Mock<Logger>().Object)
|
||||||
|
{
|
||||||
|
Definition = new IndexerDefinition
|
||||||
|
{
|
||||||
|
Settings = new TestIndexerSettings
|
||||||
|
{
|
||||||
|
MultiLanguages = new List<int> { Language.German.Id, Language.English.Id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("The.Movie.Name.2016.Multi.DTS.720p.BluRay.x264-RlsGrp")]
|
||||||
|
public void should_parse_multi_language(string postTitle)
|
||||||
|
{
|
||||||
|
var result = _indexer.CleanupReleases(new ReleaseInfo[] { new () { Title = postTitle, Languages = new List<Language>() } });
|
||||||
|
result.Single().Languages.Count.Should().Be(2);
|
||||||
|
result.Single().Languages.Should().Contain(Language.German);
|
||||||
|
result.Single().Languages.Should().Contain(Language.English);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using NLog;
|
using System.Collections.Generic;
|
||||||
|
using NLog;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.IndexerTests
|
namespace NzbDrone.Core.Test.IndexerTests
|
||||||
{
|
{
|
||||||
@@ -31,5 +33,10 @@ namespace NzbDrone.Core.Test.IndexerTests
|
|||||||
{
|
{
|
||||||
return _parser;
|
return _parser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public new IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
|
||||||
|
{
|
||||||
|
return base.CleanupReleases(releases);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
|
|||||||
[TestCase(HdrFormat.Hdr10Plus, "HDR10Plus")]
|
[TestCase(HdrFormat.Hdr10Plus, "HDR10Plus")]
|
||||||
[TestCase(HdrFormat.DolbyVision, "DV")]
|
[TestCase(HdrFormat.DolbyVision, "DV")]
|
||||||
[TestCase(HdrFormat.DolbyVisionHdr10, "DV HDR10")]
|
[TestCase(HdrFormat.DolbyVisionHdr10, "DV HDR10")]
|
||||||
|
[TestCase(HdrFormat.DolbyVisionHdr10Plus, "DV HDR10Plus")]
|
||||||
[TestCase(HdrFormat.DolbyVisionHlg, "DV HLG")]
|
[TestCase(HdrFormat.DolbyVisionHlg, "DV HLG")]
|
||||||
[TestCase(HdrFormat.DolbyVisionSdr, "DV SDR")]
|
[TestCase(HdrFormat.DolbyVisionSdr, "DV SDR")]
|
||||||
public void should_format_video_dynamic_range_type(HdrFormat format, string expectedVideoDynamicRangeType)
|
public void should_format_video_dynamic_range_type(HdrFormat format, string expectedVideoDynamicRangeType)
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
|
|||||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)]
|
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)]
|
||||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)]
|
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)]
|
||||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 1, HdrFormat.DolbyVisionHdr10)]
|
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 1, HdrFormat.DolbyVisionHdr10)]
|
||||||
|
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 1, HdrFormat.DolbyVisionHdr10Plus)]
|
||||||
|
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 6, HdrFormat.DolbyVisionHdr10Plus)]
|
||||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 2, HdrFormat.DolbyVisionSdr)]
|
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 2, HdrFormat.DolbyVisionSdr)]
|
||||||
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 4, HdrFormat.DolbyVisionHlg)]
|
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 4, HdrFormat.DolbyVisionHlg)]
|
||||||
public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, int? doviConfigId, HdrFormat expected)
|
public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, int? doviConfigId, HdrFormat expected)
|
||||||
|
|||||||
@@ -48,13 +48,14 @@ namespace NzbDrone.Core.Test.NotificationTests
|
|||||||
Subject.Definition = _traktDefinition;
|
Subject.Definition = _traktDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GiventValidMediaInfo(Quality quality, string audioChannels, string audioFormat, string scanType)
|
private void GiventValidMediaInfo(Quality quality, string audioChannels, string audioFormat, string scanType, HdrFormat hdrFormat = HdrFormat.None)
|
||||||
{
|
{
|
||||||
_downloadMessage.MovieFile.MediaInfo = new MediaInfoModel
|
_downloadMessage.MovieFile.MediaInfo = new MediaInfoModel
|
||||||
{
|
{
|
||||||
AudioChannelPositions = audioChannels,
|
AudioChannelPositions = audioChannels,
|
||||||
AudioFormat = audioFormat,
|
AudioFormat = audioFormat,
|
||||||
ScanType = scanType
|
ScanType = scanType,
|
||||||
|
VideoHdrFormat = hdrFormat
|
||||||
};
|
};
|
||||||
|
|
||||||
_downloadMessage.MovieFile.Quality.Quality = quality;
|
_downloadMessage.MovieFile.Quality.Quality = quality;
|
||||||
@@ -72,7 +73,7 @@ namespace NzbDrone.Core.Test.NotificationTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_add_collection_movie_if_valid_mediainfo()
|
public void should_add_collection_movie_if_valid_mediainfo()
|
||||||
{
|
{
|
||||||
GiventValidMediaInfo(Quality.Bluray1080p, "5.1", "DTS", "Progressive");
|
GiventValidMediaInfo(Quality.Bluray2160p, "5.1", "DTS", "Progressive", HdrFormat.DolbyVisionHdr10);
|
||||||
|
|
||||||
Subject.OnDownload(_downloadMessage);
|
Subject.OnDownload(_downloadMessage);
|
||||||
|
|
||||||
@@ -80,15 +81,16 @@ namespace NzbDrone.Core.Test.NotificationTests
|
|||||||
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
|
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
|
||||||
t.Movies.First().Audio == "dts" &&
|
t.Movies.First().Audio == "dts" &&
|
||||||
t.Movies.First().AudioChannels == "5.1" &&
|
t.Movies.First().AudioChannels == "5.1" &&
|
||||||
t.Movies.First().Resolution == "hd_1080p" &&
|
t.Movies.First().Resolution == "uhd_4k" &&
|
||||||
t.Movies.First().MediaType == "bluray"),
|
t.Movies.First().MediaType == "bluray" &&
|
||||||
|
t.Movies.First().Hdr == "hdr10"),
|
||||||
It.IsAny<string>()), Times.Once());
|
It.IsAny<string>()), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie()
|
public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie()
|
||||||
{
|
{
|
||||||
GiventValidMediaInfo(Quality.Bluray1080p, "2.0", "DTS", "Progressive");
|
GiventValidMediaInfo(Quality.Bluray2160p, "2.0", "DTS", "Progressive", HdrFormat.DolbyVisionHdr10);
|
||||||
|
|
||||||
Subject.OnDownload(_downloadMessage);
|
Subject.OnDownload(_downloadMessage);
|
||||||
|
|
||||||
@@ -96,8 +98,9 @@ namespace NzbDrone.Core.Test.NotificationTests
|
|||||||
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
|
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
|
||||||
t.Movies.First().Audio == "dts" &&
|
t.Movies.First().Audio == "dts" &&
|
||||||
t.Movies.First().AudioChannels == "2.0" &&
|
t.Movies.First().AudioChannels == "2.0" &&
|
||||||
t.Movies.First().Resolution == "hd_1080p" &&
|
t.Movies.First().Resolution == "uhd_4k" &&
|
||||||
t.Movies.First().MediaType == "bluray"),
|
t.Movies.First().MediaType == "bluray" &&
|
||||||
|
t.Movies.First().Hdr == "hdr10"),
|
||||||
It.IsAny<string>()), Times.Once());
|
It.IsAny<string>()), Times.Once());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Movie.Title.1994.Russian.1080p.XviD-LOL")]
|
[TestCase("Movie.Title.1994.Russian.1080p.XviD-LOL")]
|
||||||
|
[TestCase("Movie.Title.2020.WEB-DLRip.AVC.AC3.EN.RU.ENSub.RUSub-LOL")]
|
||||||
|
[TestCase("Movie Title (2020) WEB-DL (720p) Rus-Eng")]
|
||||||
public void should_parse_language_russian(string postTitle)
|
public void should_parse_language_russian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -300,6 +302,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Movie.Title.1994.Hebrew.1080p.XviD-LOL")]
|
[TestCase("Movie.Title.1994.Hebrew.1080p.XviD-LOL")]
|
||||||
|
[TestCase("Movie.Title.1994.1080p.BluRay.HebDubbed.Also.English.x264-P2P")]
|
||||||
public void should_parse_language_hebrew(string postTitle)
|
public void should_parse_language_hebrew(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -387,6 +390,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Movie.Title.2022.lv.WEBRip.XviD-LOL")]
|
[TestCase("Movie.Title.2022.lv.WEBRip.XviD-LOL")]
|
||||||
[TestCase("Movie.Title.2022.LATVIAN.WEBRip.XviD-LOL")]
|
[TestCase("Movie.Title.2022.LATVIAN.WEBRip.XviD-LOL")]
|
||||||
[TestCase("Movie.Title.2022.Latvian.WEBRip.XviD-LOL")]
|
[TestCase("Movie.Title.2022.Latvian.WEBRip.XviD-LOL")]
|
||||||
|
[TestCase("Movie.Title.2022.1080p.WEB-DL.DDP5.1.Atmos.H.264.Lat.Eng")]
|
||||||
|
[TestCase("Movie.Title.2022.1080p.WEB-DL.LAV.RUS-NPPK")]
|
||||||
public void should_parse_language_latvian(string postTitle)
|
public void should_parse_language_latvian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseMovieTitle(postTitle);
|
var result = Parser.Parser.ParseMovieTitle(postTitle);
|
||||||
@@ -429,5 +434,35 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
var result = LanguageParser.ParseSubtitleLanguage(fileName);
|
var result = LanguageParser.ParseSubtitleLanguage(fileName);
|
||||||
result.Should().Be(Language.Unknown);
|
result.Should().Be(Language.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("The.Movie.Name.2016.German.DTS.DL.720p.BluRay.x264-RlsGrp")]
|
||||||
|
public void should_add_original_language_to_german_release_with_dl_tag(string postTitle)
|
||||||
|
{
|
||||||
|
var result = Parser.Parser.ParseMovieTitle(postTitle);
|
||||||
|
result.Languages.Count.Should().Be(2);
|
||||||
|
result.Languages.Should().Contain(Language.German);
|
||||||
|
result.Languages.Should().Contain(Language.Original);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("The.Movie.Name.2016.GERMAN.WEB-DL.h264-RlsGrp")]
|
||||||
|
[TestCase("The.Movie.Name.2016.GERMAN.WEB.DL.h264-RlsGrp")]
|
||||||
|
[TestCase("The Movie Name 2016 GERMAN WEB DL h264-RlsGrp")]
|
||||||
|
[TestCase("The.Movie.Name.2016.GERMAN.WEBDL.h264-RlsGrp")]
|
||||||
|
public void should_not_add_original_language_to_german_release_when_title_contains_web_dl(string postTitle)
|
||||||
|
{
|
||||||
|
var result = Parser.Parser.ParseMovieTitle(postTitle);
|
||||||
|
result.Languages.Count.Should().Be(1);
|
||||||
|
result.Languages.Should().Contain(Language.German);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("The.Movie.Name.2023.German.ML.EAC3.720p.NF.WEB.H264-RlsGrp")]
|
||||||
|
public void should_add_original_language_and_english_to_german_release_with_ml_tag(string postTitle)
|
||||||
|
{
|
||||||
|
var result = Parser.Parser.ParseMovieTitle(postTitle);
|
||||||
|
result.Languages.Count.Should().Be(3);
|
||||||
|
result.Languages.Should().Contain(Language.German);
|
||||||
|
result.Languages.Should().Contain(Language.Original);
|
||||||
|
result.Languages.Should().Contain(Language.English);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("G.I.Movie.Movie.2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "G.I. Movie Movie")]
|
[TestCase("G.I.Movie.Movie.2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "G.I. Movie Movie")]
|
||||||
[TestCase("www.Torrenting.org - Movie.2008.720p.X264-DIMENSION", "Movie")]
|
[TestCase("www.Torrenting.org - Movie.2008.720p.X264-DIMENSION", "Movie")]
|
||||||
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]", "The French Movie")]
|
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]", "The French Movie")]
|
||||||
|
[TestCase("The.Good.German.2006.720p.BluRay.x264-RlsGrp", "The Good German", Description = "Hardcoded to exclude from German regex")]
|
||||||
public void should_parse_movie_title(string postTitle, string title)
|
public void should_parse_movie_title(string postTitle, string title)
|
||||||
{
|
{
|
||||||
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(title);
|
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(title);
|
||||||
@@ -124,6 +125,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Die.fantastische.Reise.des.Dr.Dolittle.2020.German.DL.LD.1080p.WEBRip.x264-PRD", "Die fantastische Reise des Dr. Dolittle", "", 2020, Description = "dot after dr")]
|
[TestCase("Die.fantastische.Reise.des.Dr.Dolittle.2020.German.DL.LD.1080p.WEBRip.x264-PRD", "Die fantastische Reise des Dr. Dolittle", "", 2020, Description = "dot after dr")]
|
||||||
[TestCase("Der.Film.deines.Lebens.German.2011.PAL.DVDR-ETM", "Der Film deines Lebens", "", 2011, Description = "year at wrong position")]
|
[TestCase("Der.Film.deines.Lebens.German.2011.PAL.DVDR-ETM", "Der Film deines Lebens", "", 2011, Description = "year at wrong position")]
|
||||||
[TestCase("Kick.Ass.2.2013.German.DTS.DL.720p.BluRay.x264-Pate_", "Kick Ass 2", "", 2013, Description = "underscore at the end")]
|
[TestCase("Kick.Ass.2.2013.German.DTS.DL.720p.BluRay.x264-Pate_", "Kick Ass 2", "", 2013, Description = "underscore at the end")]
|
||||||
|
[TestCase("The.Good.German.2006.GERMAN.720p.HDTV.x264-RLsGrp", "The Good German", "", 2006, Description = "German in the title")]
|
||||||
public void should_parse_german_movie(string postTitle, string title, string edition, int year)
|
public void should_parse_german_movie(string postTitle, string title, string edition, int year)
|
||||||
{
|
{
|
||||||
var movie = Parser.Parser.ParseMovieTitle(postTitle);
|
var movie = Parser.Parser.ParseMovieTitle(postTitle);
|
||||||
@@ -238,6 +240,10 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
|
|
||||||
[TestCase("The.Italian.Movie.2025.720p.BluRay.X264-AMIABLE")]
|
[TestCase("The.Italian.Movie.2025.720p.BluRay.X264-AMIABLE")]
|
||||||
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]")]
|
[TestCase("The.French.Movie.2013.720p.BluRay.x264 - ROUGH[PublicHD]")]
|
||||||
|
[TestCase("The.German.Doctor.2013.LIMITED.DVDRip.x264-RedBlade", Description = "When German is not followed by a year or a SCENE word it is not matched")]
|
||||||
|
[TestCase("The.Good.German.2006.720p.HDTV.x264-TVP", Description = "The Good German is hardcoded not to match")]
|
||||||
|
[TestCase("German.Lancers.2019.720p.BluRay.x264-UNiVERSUM", Description = "German at the beginning is never matched")]
|
||||||
|
[TestCase("The.German.2019.720p.BluRay.x264-UNiVERSUM", Description = "The German is hardcoded not to match")]
|
||||||
public void should_not_parse_wrong_language_in_title(string postTitle)
|
public void should_not_parse_wrong_language_in_title(string postTitle)
|
||||||
{
|
{
|
||||||
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
|
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||||
@@ -245,22 +251,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
parsed.Languages.First().Should().Be(Language.Unknown);
|
parsed.Languages.First().Should().Be(Language.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("The.Movie.Name.2016.German.DTS.DL.720p.BluRay.x264-MULTiPLEX")]
|
|
||||||
public void should_not_parse_multi_language_in_releasegroup(string postTitle)
|
|
||||||
{
|
|
||||||
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
|
|
||||||
parsed.Languages.Count.Should().Be(1);
|
|
||||||
parsed.Languages.First().Should().Be(Language.German);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("The.Movie.Name.2016.German.Multi.DTS.DL.720p.BluRay.x264-MULTiPLEX")]
|
|
||||||
public void should_parse_multi_language(string postTitle)
|
|
||||||
{
|
|
||||||
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
|
|
||||||
parsed.Languages.Count.Should().Be(1);
|
|
||||||
parsed.Languages.Should().Contain(Language.German);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("Movie.Title.2016.1080p.KORSUB.WEBRip.x264.AAC2.0-RADARR", "KORSUB")]
|
[TestCase("Movie.Title.2016.1080p.KORSUB.WEBRip.x264.AAC2.0-RADARR", "KORSUB")]
|
||||||
[TestCase("Movie.Title.2016.1080p.KORSUBS.WEBRip.x264.AAC2.0-RADARR", "KORSUBS")]
|
[TestCase("Movie.Title.2016.1080p.KORSUBS.WEBRip.x264.AAC2.0-RADARR", "KORSUBS")]
|
||||||
[TestCase("Movie Title 2017 HC 720p HDRiP DD5 1 x264-LEGi0N", "Generic Hardcoded Subs")]
|
[TestCase("Movie Title 2017 HC 720p HDRiP DD5 1 x264-LEGi0N", "Generic Hardcoded Subs")]
|
||||||
|
|||||||
@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
|
|||||||
public string Hint { get; set; }
|
public string Hint { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||||
|
public class FieldTokenAttribute : Attribute
|
||||||
|
{
|
||||||
|
public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
Field = field;
|
||||||
|
Token = token;
|
||||||
|
Value = value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Label { get; set; }
|
||||||
|
public TokenField Field { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class FieldSelectOption
|
public class FieldSelectOption
|
||||||
{
|
{
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
@@ -84,4 +101,11 @@ namespace NzbDrone.Core.Annotations
|
|||||||
ApiKey,
|
ApiKey,
|
||||||
UserName
|
UserName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TokenField
|
||||||
|
{
|
||||||
|
Label,
|
||||||
|
HelpText,
|
||||||
|
HelpTextWarning
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
public interface IBlocklistService
|
public interface IBlocklistService
|
||||||
{
|
{
|
||||||
bool Blocklisted(int movieId, ReleaseInfo release);
|
bool Blocklisted(int movieId, ReleaseInfo release);
|
||||||
|
bool BlocklistedTorrentHash(int movieId, string hash);
|
||||||
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
||||||
List<Blocklist> GetByMovieId(int movieId);
|
List<Blocklist> GetByMovieId(int movieId);
|
||||||
void Block(RemoteMovie remoteMovie, string message);
|
void Block(RemoteMovie remoteMovie, string message);
|
||||||
@@ -37,30 +38,34 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
|
|
||||||
public bool Blocklisted(int movieId, ReleaseInfo release)
|
public bool Blocklisted(int movieId, ReleaseInfo release)
|
||||||
{
|
{
|
||||||
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(movieId, release.Title);
|
|
||||||
|
|
||||||
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||||
{
|
{
|
||||||
var torrentInfo = release as TorrentInfo;
|
if (release is not TorrentInfo torrentInfo)
|
||||||
|
|
||||||
if (torrentInfo == null)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
|
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash);
|
||||||
.Any(b => SameTorrent(b, torrentInfo));
|
|
||||||
|
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, torrentInfo.InfoHash);
|
return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
|
||||||
|
.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
||||||
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
.Any(b => SameTorrent(b, torrentInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
return _blocklistRepository.BlocklistedByTitle(movieId, release.Title)
|
||||||
.Any(b => SameNzb(b, release));
|
.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
||||||
|
.Any(b => SameNzb(b, release));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BlocklistedTorrentHash(int movieId, string hash)
|
||||||
|
{
|
||||||
|
return _blocklistRepository.BlocklistedByTorrentInfoHash(movieId, hash).Any(b =>
|
||||||
|
b.TorrentInfoHash.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
|
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
|
||||||
|
|||||||
@@ -330,8 +330,8 @@ namespace NzbDrone.Core.Configuration
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If SSL is enabled and a cert hash is still in the config file disable SSL
|
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
|
||||||
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
|
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||||
{
|
{
|
||||||
SetValue("EnableSsl", false);
|
SetValue("EnableSsl", false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.CustomFormats
|
||||||
|
{
|
||||||
|
public class YearSpecificationValidator : AbstractValidator<YearSpecification>
|
||||||
|
{
|
||||||
|
public YearSpecificationValidator()
|
||||||
|
{
|
||||||
|
RuleFor(c => c.Min).NotEmpty().GreaterThan(0);
|
||||||
|
RuleFor(c => c.Max).NotEmpty().GreaterThanOrEqualTo(c => c.Min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class YearSpecification : CustomFormatSpecificationBase
|
||||||
|
{
|
||||||
|
private static readonly YearSpecificationValidator Validator = new ();
|
||||||
|
|
||||||
|
public override int Order => 10;
|
||||||
|
public override string ImplementationName => "Year";
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "Minimum Year", Type = FieldType.Number)]
|
||||||
|
public int Min { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(2, Label = "Maximum Year", Type = FieldType.Number)]
|
||||||
|
public int Max { get; set; }
|
||||||
|
|
||||||
|
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||||
|
{
|
||||||
|
var year = input.MovieInfo?.Year ?? input.Movie?.MovieMetadata?.Value?.Year;
|
||||||
|
|
||||||
|
return year >= Min && year <= Max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override NzbDroneValidationResult Validate()
|
||||||
|
{
|
||||||
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
public interface IConnectionStringFactory
|
public interface IConnectionStringFactory
|
||||||
{
|
{
|
||||||
string MainDbConnectionString { get; }
|
DatabaseConnectionInfo MainDbConnection { get; }
|
||||||
string LogDbConnectionString { get; }
|
DatabaseConnectionInfo LogDbConnection { get; }
|
||||||
string GetDatabasePath(string connectionString);
|
string GetDatabasePath(string connectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
|
|
||||||
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||||
GetConnectionString(appFolderInfo.GetDatabase());
|
GetConnectionString(appFolderInfo.GetDatabase());
|
||||||
|
|
||||||
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||||
GetConnectionString(appFolderInfo.GetLogDatabase());
|
GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MainDbConnectionString { get; private set; }
|
public DatabaseConnectionInfo MainDbConnection { get; private set; }
|
||||||
public string LogDbConnectionString { get; private set; }
|
public DatabaseConnectionInfo LogDbConnection { get; private set; }
|
||||||
|
|
||||||
public string GetDatabasePath(string connectionString)
|
public string GetDatabasePath(string connectionString)
|
||||||
{
|
{
|
||||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
return connectionBuilder.DataSource;
|
return connectionBuilder.DataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetConnectionString(string dbPath)
|
private static DatabaseConnectionInfo GetConnectionString(string dbPath)
|
||||||
{
|
{
|
||||||
var connectionBuilder = new SQLiteConnectionStringBuilder
|
var connectionBuilder = new SQLiteConnectionStringBuilder
|
||||||
{
|
{
|
||||||
@@ -57,21 +57,22 @@ namespace NzbDrone.Core.Datastore
|
|||||||
connectionBuilder.Add("Full FSync", true);
|
connectionBuilder.Add("Full FSync", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return connectionBuilder.ConnectionString;
|
return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetPostgresConnectionString(string dbName)
|
private DatabaseConnectionInfo GetPostgresConnectionString(string dbName)
|
||||||
{
|
{
|
||||||
var connectionBuilder = new NpgsqlConnectionStringBuilder();
|
var connectionBuilder = new NpgsqlConnectionStringBuilder
|
||||||
|
{
|
||||||
|
Database = dbName,
|
||||||
|
Host = _configFileProvider.PostgresHost,
|
||||||
|
Username = _configFileProvider.PostgresUser,
|
||||||
|
Password = _configFileProvider.PostgresPassword,
|
||||||
|
Port = _configFileProvider.PostgresPort,
|
||||||
|
Enlist = false
|
||||||
|
};
|
||||||
|
|
||||||
connectionBuilder.Database = dbName;
|
return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString);
|
||||||
connectionBuilder.Host = _configFileProvider.PostgresHost;
|
|
||||||
connectionBuilder.Username = _configFileProvider.PostgresUser;
|
|
||||||
connectionBuilder.Password = _configFileProvider.PostgresPassword;
|
|
||||||
connectionBuilder.Port = _configFileProvider.PostgresPort;
|
|
||||||
connectionBuilder.Enlist = false;
|
|
||||||
|
|
||||||
return connectionBuilder.ConnectionString;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace NzbDrone.Core.Datastore
|
||||||
|
{
|
||||||
|
public class DatabaseConnectionInfo
|
||||||
|
{
|
||||||
|
public DatabaseConnectionInfo(DatabaseType databaseType, string connectionString)
|
||||||
|
{
|
||||||
|
DatabaseType = databaseType;
|
||||||
|
ConnectionString = connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseType DatabaseType { get; internal set; }
|
||||||
|
public string ConnectionString { get; internal set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Data.SQLite;
|
using System.Data.SQLite;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
using NLog;
|
using NLog;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
@@ -60,22 +61,22 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
public IDatabase Create(MigrationContext migrationContext)
|
public IDatabase Create(MigrationContext migrationContext)
|
||||||
{
|
{
|
||||||
string connectionString;
|
DatabaseConnectionInfo connectionInfo;
|
||||||
|
|
||||||
switch (migrationContext.MigrationType)
|
switch (migrationContext.MigrationType)
|
||||||
{
|
{
|
||||||
case MigrationType.Main:
|
case MigrationType.Main:
|
||||||
{
|
{
|
||||||
connectionString = _connectionStringFactory.MainDbConnectionString;
|
connectionInfo = _connectionStringFactory.MainDbConnection;
|
||||||
CreateMain(connectionString, migrationContext);
|
CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case MigrationType.Log:
|
case MigrationType.Log:
|
||||||
{
|
{
|
||||||
connectionString = _connectionStringFactory.LogDbConnectionString;
|
connectionInfo = _connectionStringFactory.LogDbConnection;
|
||||||
CreateLog(connectionString, migrationContext);
|
CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -90,14 +91,14 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
DbConnection conn;
|
DbConnection conn;
|
||||||
|
|
||||||
if (connectionString.Contains(".db"))
|
if (connectionInfo.DatabaseType == DatabaseType.SQLite)
|
||||||
{
|
{
|
||||||
conn = SQLiteFactory.Instance.CreateConnection();
|
conn = SQLiteFactory.Instance.CreateConnection();
|
||||||
conn.ConnectionString = connectionString;
|
conn.ConnectionString = connectionInfo.ConnectionString;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
conn = new NpgsqlConnection(connectionString);
|
conn = new NpgsqlConnection(connectionInfo.ConnectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Open();
|
conn.Open();
|
||||||
@@ -107,12 +108,12 @@ namespace NzbDrone.Core.Datastore
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateMain(string connectionString, MigrationContext migrationContext)
|
private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_restoreDatabaseService.Restore();
|
_restoreDatabaseService.Restore();
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
}
|
}
|
||||||
catch (SQLiteException e)
|
catch (SQLiteException e)
|
||||||
{
|
{
|
||||||
@@ -135,15 +136,17 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
||||||
|
|
||||||
|
Thread.Sleep(5000);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (--retryCount > 0)
|
if (--retryCount > 0)
|
||||||
{
|
{
|
||||||
System.Threading.Thread.Sleep(5000);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +165,11 @@ namespace NzbDrone.Core.Datastore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateLog(string connectionString, MigrationContext migrationContext)
|
private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
}
|
}
|
||||||
catch (SQLiteException e)
|
catch (SQLiteException e)
|
||||||
{
|
{
|
||||||
@@ -186,7 +189,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
|
Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_migrationController.Migrate(connectionString, migrationContext);
|
_migrationController.Migrate(connectionString, migrationContext, databaseType);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visits the memeber access expression. To be implemented by user.
|
/// Visits the member access expression. To be implemented by user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="expression"></param>
|
/// <param name="expression"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(234)]
|
||||||
|
public class movie_last_searched_time : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("Movies").AddColumn("LastSearchTime").AsDateTimeOffset().Nullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
{
|
{
|
||||||
public interface IMigrationController
|
public interface IMigrationController
|
||||||
{
|
{
|
||||||
void Migrate(string connectionString, MigrationContext migrationContext);
|
void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MigrationController : IMigrationController
|
public class MigrationController : IMigrationController
|
||||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
_migrationLoggerProvider = migrationLoggerProvider;
|
_migrationLoggerProvider = migrationLoggerProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Migrate(string connectionString, MigrationContext migrationContext)
|
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
ServiceProvider serviceProvider;
|
ServiceProvider serviceProvider;
|
||||||
|
|
||||||
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
|
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
||||||
|
|
||||||
serviceProvider = new ServiceCollection()
|
serviceProvider = new ServiceCollection()
|
||||||
.AddLogging(b => b.AddNLog())
|
.AddLogging(b => b.AddNLog())
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||||||
private int ComparePeersIfTorrent(DownloadDecision x, DownloadDecision y)
|
private int ComparePeersIfTorrent(DownloadDecision x, DownloadDecision y)
|
||||||
{
|
{
|
||||||
// Different protocols should get caught when checking the preferred protocol,
|
// Different protocols should get caught when checking the preferred protocol,
|
||||||
// since we're dealing with the same movie in our comparisions
|
// since we're dealing with the same movie in our comparisons
|
||||||
if (x.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent ||
|
if (x.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent ||
|
||||||
y.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent)
|
y.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -78,7 +78,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug("New item has a better custom format score");
|
_logger.Debug("New item's custom formats [{0}] ({1}) improve on [{2}] ({3}), accepting",
|
||||||
|
newCustomFormats.ConcatToString(),
|
||||||
|
newFormatScore,
|
||||||
|
currentCustomFormats.ConcatToString(),
|
||||||
|
currentFormatScore);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -26,11 +26,11 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using System.Xml.XPath;
|
using System.Xml.XPath;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Download.Extensions;
|
using NzbDrone.Core.Download.Extensions;
|
||||||
|
|
||||||
@@ -97,8 +98,14 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
|||||||
|
|
||||||
public string AddMagnet(Aria2Settings settings, string magnet)
|
public string AddMagnet(Aria2Settings settings, string magnet)
|
||||||
{
|
{
|
||||||
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet });
|
var options = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
if (settings.Directory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
options.Add("dir", settings.Directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet }, options);
|
||||||
var gid = response.GetStringResponse();
|
var gid = response.GetStringResponse();
|
||||||
|
|
||||||
return gid;
|
return gid;
|
||||||
@@ -106,8 +113,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
|||||||
|
|
||||||
public string AddTorrent(Aria2Settings settings, byte[] torrent)
|
public string AddTorrent(Aria2Settings settings, byte[] torrent)
|
||||||
{
|
{
|
||||||
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent);
|
// Aria2's second parameter is an array of URIs and needs to be sent if options are provided, this satisfies that requirement.
|
||||||
|
var emptyListOfUris = new List<string>();
|
||||||
|
var options = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
if (settings.Directory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
options.Add("dir", settings.Directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent, emptyListOfUris, options);
|
||||||
var gid = response.GetStringResponse();
|
var gid = response.GetStringResponse();
|
||||||
|
|
||||||
return gid;
|
return gid;
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ namespace NzbDrone.Core.Download.Clients.Aria2
|
|||||||
|
|
||||||
[FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
[FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||||
public string SecretToken { get; set; }
|
public string SecretToken { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")]
|
||||||
|
public string Directory { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
{
|
{
|
||||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
|||||||
@@ -23,15 +23,13 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
|||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly IDiskScanService _diskScanService;
|
private readonly IDiskScanService _diskScanService;
|
||||||
private readonly INamingConfigService _namingConfigService;
|
|
||||||
private readonly ICached<Dictionary<string, WatchFolderItem>> _watchFolderItemCache;
|
private readonly ICached<Dictionary<string, WatchFolderItem>> _watchFolderItemCache;
|
||||||
|
|
||||||
public ScanWatchFolder(ICacheManager cacheManager, IDiskScanService diskScanService, INamingConfigService namingConfigService, IDiskProvider diskProvider, Logger logger)
|
public ScanWatchFolder(ICacheManager cacheManager, IDiskScanService diskScanService, IDiskProvider diskProvider, Logger logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_diskScanService = diskScanService;
|
_diskScanService = diskScanService;
|
||||||
_namingConfigService = namingConfigService;
|
|
||||||
_watchFolderItemCache = cacheManager.GetCache<Dictionary<string, WatchFolderItem>>(GetType());
|
_watchFolderItemCache = cacheManager.GetCache<Dictionary<string, WatchFolderItem>>(GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
using NzbDrone.Core.Organizer;
|
||||||
@@ -27,11 +28,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_scanWatchFolder = scanWatchFolder;
|
_scanWatchFolder = scanWatchFolder;
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
|
|||||||
public UsenetBlackhole(IScanWatchFolder scanWatchFolder,
|
public UsenetBlackhole(IScanWatchFolder scanWatchFolder,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IValidateNzbs nzbValidationService,
|
IValidateNzbs nzbValidationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
||||||
{
|
{
|
||||||
_scanWatchFolder = scanWatchFolder;
|
_scanWatchFolder = scanWatchFolder;
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -24,11 +24,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
@@ -35,11 +35,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_dsInfoProxy = dsInfoProxy;
|
_dsInfoProxy = dsInfoProxy;
|
||||||
_dsTaskProxySelector = dsTaskProxySelector;
|
_dsTaskProxySelector = dsTaskProxySelector;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using NzbDrone.Common.Extensions;
|
|||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
@@ -32,12 +31,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
IDownloadStationTaskProxySelector dsTaskProxySelector,
|
IDownloadStationTaskProxySelector dsTaskProxySelector,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IValidateNzbs nzbValidationService,
|
IValidateNzbs nzbValidationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
||||||
{
|
{
|
||||||
_dsInfoProxy = dsInfoProxy;
|
_dsInfoProxy = dsInfoProxy;
|
||||||
_dsTaskProxySelector = dsTaskProxySelector;
|
_dsTaskProxySelector = dsTaskProxySelector;
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.Flood.Models;
|
using NzbDrone.Core.Download.Clients.Flood.Models;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
@@ -26,11 +26,11 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
_downloadSeedConfigProvider = downloadSeedConfigProvider;
|
_downloadSeedConfigProvider = downloadSeedConfigProvider;
|
||||||
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.Where(t => t.IsNotNullOrWhiteSpace());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Name => "Flood";
|
public override string Name => "Flood";
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
|
|
||||||
@@ -21,15 +20,14 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
|||||||
private readonly IFreeboxDownloadProxy _proxy;
|
private readonly IFreeboxDownloadProxy _proxy;
|
||||||
|
|
||||||
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
|
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
|
||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
IDiskProvider diskProvider,
|
||||||
IDiskProvider diskProvider,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IBlocklistService blocklistService,
|
||||||
ICacheManager cacheManager,
|
Logger logger)
|
||||||
Logger logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.Hadouken.Models;
|
using NzbDrone.Core.Download.Clients.Hadouken.Models;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -24,11 +24,11 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using NzbDrone.Common.Disk;
|
|||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -22,12 +21,11 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
|
|||||||
public NzbVortex(INzbVortexProxy proxy,
|
public NzbVortex(INzbVortexProxy proxy,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IValidateNzbs nzbValidationService,
|
IValidateNzbs nzbValidationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using NzbDrone.Common.Extensions;
|
|||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -26,12 +25,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
public Nzbget(INzbgetProxy proxy,
|
public Nzbget(INzbgetProxy proxy,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IValidateNzbs nzbValidationService,
|
IValidateNzbs nzbValidationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
|
|||||||
|
|
||||||
public Pneumatic(IHttpClient httpClient,
|
public Pneumatic(IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(configService, diskProvider, remotePathMappingService, logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ using NzbDrone.Common.Cache;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -32,12 +32,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxySelector = proxySelector;
|
_proxySelector = proxySelector;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
|
{
|
||||||
|
public enum QBittorrentContentLayout
|
||||||
|
{
|
||||||
|
Default = 0,
|
||||||
|
Original = 1,
|
||||||
|
Subfolder = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -265,6 +265,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
request.AddFormParameter("firstLastPiecePrio", true);
|
request.AddFormParameter("firstLastPiecePrio", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Original)
|
||||||
|
{
|
||||||
|
request.AddFormParameter("contentLayout", "Original");
|
||||||
|
}
|
||||||
|
else if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Subfolder)
|
||||||
|
{
|
||||||
|
request.AddFormParameter("contentLayout", "Subfolder");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
[FieldDefinition(12, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
|
[FieldDefinition(12, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
|
||||||
public bool FirstAndLast { get; set; }
|
public bool FirstAndLast { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
|
||||||
|
public int ContentLayout { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
{
|
{
|
||||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using NzbDrone.Common.Extensions;
|
|||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -24,12 +23,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
public Sabnzbd(ISabnzbdProxy proxy,
|
public Sabnzbd(ISabnzbdProxy proxy,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IValidateNzbs nzbValidationService,
|
IValidateNzbs nzbValidationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
: base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ using FluentValidation.Results;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Transmission
|
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||||
@@ -17,11 +17,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(proxy, torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -23,11 +23,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
|
|
||||||
private TransmissionResponse GetSessionVariables(TransmissionSettings settings)
|
private TransmissionResponse GetSessionVariables(TransmissionSettings settings)
|
||||||
{
|
{
|
||||||
// Retrieve transmission information such as the default download directory, bandwith throttling and seed ratio.
|
// Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio.
|
||||||
|
|
||||||
return ProcessRequest("session-get", null, settings);
|
return ProcessRequest("session-get", null, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ using FluentValidation.Results;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.Transmission;
|
using NzbDrone.Core.Download.Clients.Transmission;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Vuze
|
namespace NzbDrone.Core.Download.Clients.Vuze
|
||||||
@@ -18,11 +18,11 @@ namespace NzbDrone.Core.Download.Clients.Vuze
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(proxy, torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ using NzbDrone.Common.Disk;
|
|||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download.Clients.rTorrent;
|
using NzbDrone.Core.Download.Clients.rTorrent;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
@@ -31,13 +31,13 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
IDownloadSeedConfigProvider downloadSeedConfigProvider,
|
IDownloadSeedConfigProvider downloadSeedConfigProvider,
|
||||||
IRTorrentDirectoryValidator rTorrentDirectoryValidator,
|
IRTorrentDirectoryValidator rTorrentDirectoryValidator,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
|
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ using NzbDrone.Common.Cache;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -27,11 +27,11 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
|||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, blocklistService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Retry;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
{
|
{
|
||||||
@@ -18,11 +21,41 @@ namespace NzbDrone.Core.Download
|
|||||||
where TSettings : IProviderConfig, new()
|
where TSettings : IProviderConfig, new()
|
||||||
{
|
{
|
||||||
protected readonly IConfigService _configService;
|
protected readonly IConfigService _configService;
|
||||||
protected readonly INamingConfigService _namingConfigService;
|
|
||||||
protected readonly IDiskProvider _diskProvider;
|
protected readonly IDiskProvider _diskProvider;
|
||||||
protected readonly IRemotePathMappingService _remotePathMappingService;
|
protected readonly IRemotePathMappingService _remotePathMappingService;
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
|
|
||||||
|
protected ResiliencePipeline<HttpResponse> RetryStrategy => new ResiliencePipelineBuilder<HttpResponse>()
|
||||||
|
.AddRetry(new RetryStrategyOptions<HttpResponse>
|
||||||
|
{
|
||||||
|
ShouldHandle = static args => args.Outcome switch
|
||||||
|
{
|
||||||
|
{ Result.HasHttpServerError: true } => PredicateResult.True(),
|
||||||
|
{ Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(),
|
||||||
|
_ => PredicateResult.False()
|
||||||
|
},
|
||||||
|
Delay = TimeSpan.FromSeconds(3),
|
||||||
|
MaxRetryAttempts = 2,
|
||||||
|
BackoffType = DelayBackoffType.Exponential,
|
||||||
|
UseJitter = true,
|
||||||
|
OnRetry = args =>
|
||||||
|
{
|
||||||
|
var exception = args.Outcome.Exception;
|
||||||
|
|
||||||
|
if (exception is not null)
|
||||||
|
{
|
||||||
|
_logger.Info(exception, "Request for {0} failed with exception '{1}'. Retrying in {2}s.", Definition.Name, exception.Message, args.RetryDelay.TotalSeconds);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Info("Request for {0} failed with status {1}. Retrying in {2}s.", Definition.Name, args.Outcome.Result?.StatusCode, args.RetryDelay.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
public abstract string Name { get; }
|
public abstract string Name { get; }
|
||||||
|
|
||||||
public Type ConfigContract => typeof(TSettings);
|
public Type ConfigContract => typeof(TSettings);
|
||||||
@@ -41,13 +74,11 @@ namespace NzbDrone.Core.Download
|
|||||||
protected TSettings Settings => (TSettings)Definition.Settings;
|
protected TSettings Settings => (TSettings)Definition.Settings;
|
||||||
|
|
||||||
protected DownloadClientBase(IConfigService configService,
|
protected DownloadClientBase(IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_namingConfigService = namingConfigService;
|
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_remotePathMappingService = remotePathMappingService;
|
_remotePathMappingService = remotePathMappingService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -58,10 +89,7 @@ namespace NzbDrone.Core.Download
|
|||||||
return GetType().Name;
|
return GetType().Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract DownloadProtocol Protocol
|
public abstract DownloadProtocol Protocol { get; }
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Task<string> Download(RemoteMovie remoteMovie, IIndexer indexer);
|
public abstract Task<string> Download(RemoteMovie remoteMovie, IIndexer indexer);
|
||||||
public abstract IEnumerable<DownloadClientItem> GetItems();
|
public abstract IEnumerable<DownloadClientItem> GetItems();
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ namespace NzbDrone.Core.Download
|
|||||||
_logger.Trace("Release {0} no longer available on indexer.", remoteMovie);
|
_logger.Trace("Release {0} no longer available on indexer.", remoteMovie);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
catch (ReleaseBlockedException)
|
||||||
|
{
|
||||||
|
_logger.Trace("Release {0} previously added to blocklist, not sending to download client again.", remoteMovie);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (DownloadClientRejectedReleaseException)
|
catch (DownloadClientRejectedReleaseException)
|
||||||
{
|
{
|
||||||
_logger.Trace("Release {0} rejected by download client, possible duplicate.", remoteMovie);
|
_logger.Trace("Release {0} rejected by download client, possible duplicate.", remoteMovie);
|
||||||
@@ -127,7 +132,7 @@ namespace NzbDrone.Core.Download
|
|||||||
movieGrabbedEvent.DownloadClientId = downloadClient.Definition.Id;
|
movieGrabbedEvent.DownloadClientId = downloadClient.Definition.Id;
|
||||||
movieGrabbedEvent.DownloadClientName = downloadClient.Definition.Name;
|
movieGrabbedEvent.DownloadClientName = downloadClient.Definition.Name;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(downloadClientId))
|
if (downloadClientId.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
movieGrabbedEvent.DownloadId = downloadClientId;
|
movieGrabbedEvent.DownloadId = downloadClientId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
private void PublishDownloadFailedEvent(List<MovieHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
|
private void PublishDownloadFailedEvent(List<MovieHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
|
||||||
{
|
{
|
||||||
var historyItem = historyItems.First();
|
var historyItem = historyItems.Last();
|
||||||
Enum.TryParse(historyItem.Data.GetValueOrDefault(MovieHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
|
Enum.TryParse(historyItem.Data.GetValueOrDefault(MovieHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
|
||||||
|
|
||||||
var downloadFailedEvent = new DownloadFailedEvent
|
var downloadFailedEvent = new DownloadFailedEvent
|
||||||
|
|||||||
@@ -326,7 +326,8 @@ namespace NzbDrone.Core.Download.Pending
|
|||||||
Reason = reason,
|
Reason = reason,
|
||||||
AdditionalInfo = new PendingReleaseAdditionalInfo
|
AdditionalInfo = new PendingReleaseAdditionalInfo
|
||||||
{
|
{
|
||||||
MovieMatchType = decision.RemoteMovie.MovieMatchType
|
MovieMatchType = decision.RemoteMovie.MovieMatchType,
|
||||||
|
ReleaseSource = decision.RemoteMovie.ReleaseSource
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using NLog;
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Blocklisting;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
@@ -21,18 +22,20 @@ namespace NzbDrone.Core.Download
|
|||||||
where TSettings : IProviderConfig, new()
|
where TSettings : IProviderConfig, new()
|
||||||
{
|
{
|
||||||
protected readonly IHttpClient _httpClient;
|
protected readonly IHttpClient _httpClient;
|
||||||
|
private readonly IBlocklistService _blocklistService;
|
||||||
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
|
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
|
||||||
|
|
||||||
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
|
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
INamingConfigService namingConfigService,
|
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IRemotePathMappingService remotePathMappingService,
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
IBlocklistService blocklistService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(configService, namingConfigService, diskProvider, remotePathMappingService, logger)
|
: base(configService, diskProvider, remotePathMappingService, logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
|
_blocklistService = blocklistService;
|
||||||
_torrentFileInfoReader = torrentFileInfoReader;
|
_torrentFileInfoReader = torrentFileInfoReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +90,7 @@ namespace NzbDrone.Core.Download
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return DownloadFromMagnetUrl(remoteMovie, magnetUrl);
|
return DownloadFromMagnetUrl(remoteMovie, indexer, magnetUrl);
|
||||||
}
|
}
|
||||||
catch (NotSupportedException ex)
|
catch (NotSupportedException ex)
|
||||||
{
|
{
|
||||||
@@ -101,7 +104,7 @@ namespace NzbDrone.Core.Download
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return DownloadFromMagnetUrl(remoteMovie, magnetUrl);
|
return DownloadFromMagnetUrl(remoteMovie, indexer, magnetUrl);
|
||||||
}
|
}
|
||||||
catch (NotSupportedException ex)
|
catch (NotSupportedException ex)
|
||||||
{
|
{
|
||||||
@@ -134,7 +137,9 @@ namespace NzbDrone.Core.Download
|
|||||||
request.Headers.Accept = "application/x-bittorrent";
|
request.Headers.Accept = "application/x-bittorrent";
|
||||||
request.AllowAutoRedirect = false;
|
request.AllowAutoRedirect = false;
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync(request);
|
var response = await RetryStrategy
|
||||||
|
.ExecuteAsync(static async (state, _) => await state._httpClient.GetAsync(state.request), (_httpClient, request))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
|
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
|
||||||
response.StatusCode == HttpStatusCode.Found ||
|
response.StatusCode == HttpStatusCode.Found ||
|
||||||
@@ -148,7 +153,7 @@ namespace NzbDrone.Core.Download
|
|||||||
{
|
{
|
||||||
if (locationHeader.StartsWith("magnet:"))
|
if (locationHeader.StartsWith("magnet:"))
|
||||||
{
|
{
|
||||||
return DownloadFromMagnetUrl(remoteMovie, locationHeader);
|
return DownloadFromMagnetUrl(remoteMovie, indexer, locationHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Url += new HttpUri(locationHeader);
|
request.Url += new HttpUri(locationHeader);
|
||||||
@@ -191,6 +196,9 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteMovie.Release.Title));
|
var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteMovie.Release.Title));
|
||||||
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
|
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
|
||||||
|
|
||||||
|
EnsureReleaseIsNotBlocklisted(remoteMovie, indexer, hash);
|
||||||
|
|
||||||
var actualHash = AddFromTorrentFile(remoteMovie, hash, filename, torrentFile);
|
var actualHash = AddFromTorrentFile(remoteMovie, hash, filename, torrentFile);
|
||||||
|
|
||||||
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
|
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
|
||||||
@@ -204,7 +212,7 @@ namespace NzbDrone.Core.Download
|
|||||||
return actualHash;
|
return actualHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string DownloadFromMagnetUrl(RemoteMovie remoteMovie, string magnetUrl)
|
private string DownloadFromMagnetUrl(RemoteMovie remoteMovie, IIndexer indexer, string magnetUrl)
|
||||||
{
|
{
|
||||||
string hash = null;
|
string hash = null;
|
||||||
string actualHash = null;
|
string actualHash = null;
|
||||||
@@ -222,6 +230,8 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
if (hash != null)
|
if (hash != null)
|
||||||
{
|
{
|
||||||
|
EnsureReleaseIsNotBlocklisted(remoteMovie, indexer, hash);
|
||||||
|
|
||||||
actualHash = AddFromMagnetLink(remoteMovie, hash, magnetUrl);
|
actualHash = AddFromMagnetLink(remoteMovie, hash, magnetUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,5 +245,30 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
return actualHash;
|
return actualHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureReleaseIsNotBlocklisted(RemoteMovie remoteMovie, IIndexer indexer, string hash)
|
||||||
|
{
|
||||||
|
var indexerSettings = indexer?.Definition?.Settings as ITorrentIndexerSettings;
|
||||||
|
var torrentInfo = remoteMovie.Release as TorrentInfo;
|
||||||
|
var torrentInfoHash = torrentInfo?.InfoHash;
|
||||||
|
|
||||||
|
// If the release didn't come from an interactive search,
|
||||||
|
// the hash wasn't known during processing and the
|
||||||
|
// indexer is configured to reject blocklisted releases
|
||||||
|
// during grab check if it's already been blocklisted.
|
||||||
|
|
||||||
|
if (torrentInfo != null && torrentInfoHash.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
// If the hash isn't known from parsing we set it here so it can be used for blocklisting.
|
||||||
|
torrentInfo.InfoHash = hash;
|
||||||
|
|
||||||
|
if (remoteMovie.ReleaseSource != ReleaseSourceType.InteractiveSearch &&
|
||||||
|
indexerSettings?.RejectBlocklistedTorrentHashesWhileGrabbing == true &&
|
||||||
|
_blocklistService.BlocklistedTorrentHash(remoteMovie.Movie.Id, hash))
|
||||||
|
{
|
||||||
|
throw new ReleaseBlockedException(remoteMovie.Release, "Release previously added to blocklist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user