mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-13 20:44:52 -04:00
Compare commits
73 Commits
v3.1.1.495
...
v3.2.2.508
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bca1a71a2 | ||
|
|
4f009bb81d | ||
|
|
bdc7733faf | ||
|
|
7f4be53db0 | ||
|
|
27d998d6f2 | ||
|
|
5eb593f453 | ||
|
|
4652db0583 | ||
|
|
35d43480bf | ||
|
|
3e7c136a7f | ||
|
|
05f9f6b413 | ||
|
|
67f6eb544a | ||
|
|
3c11e934a8 | ||
|
|
627a39b8fc | ||
|
|
95c7b96dff | ||
|
|
dadd59fc3a | ||
|
|
e67d3d3666 | ||
|
|
f4718243ed | ||
|
|
fcec787eb6 | ||
|
|
5fe8f65d64 | ||
|
|
c2a21cd238 | ||
|
|
a31ca4e80b | ||
|
|
db14ac4605 | ||
|
|
5f229b78be | ||
|
|
543f2e7ddc | ||
|
|
d6b7ab6260 | ||
|
|
d7ab9292fb | ||
|
|
4300d8d8c6 | ||
|
|
446b2ffff9 | ||
|
|
695720b552 | ||
|
|
c47934c5ca | ||
|
|
9938737cd7 | ||
|
|
58326f05e0 | ||
|
|
04ad5ec9c0 | ||
|
|
2c008384dd | ||
|
|
a9b605c872 | ||
|
|
cd9b469823 | ||
|
|
df4bfa501c | ||
|
|
194a1e5154 | ||
|
|
e53b2bb83c | ||
|
|
10772c09ef | ||
|
|
f9ed15409a | ||
|
|
f75ab93458 | ||
|
|
7755a8bd3b | ||
|
|
017c7998be | ||
|
|
f40ddfef10 | ||
|
|
80049909eb | ||
|
|
fc22264f89 | ||
|
|
aba2e10b5c | ||
|
|
4b6874d551 | ||
|
|
58934a30ce | ||
|
|
33a960f325 | ||
|
|
8560ff43fe | ||
|
|
c88a47b275 | ||
|
|
67fe9101d9 | ||
|
|
af99c78352 | ||
|
|
df3253f55c | ||
|
|
b9abc1be11 | ||
|
|
5c0ee04271 | ||
|
|
5e2cd3798b | ||
|
|
83041b1d37 | ||
|
|
9f6c48191b | ||
|
|
5696fa2efe | ||
|
|
d38311b717 | ||
|
|
aa522066ee | ||
|
|
d2ba70c4d7 | ||
|
|
ca2e62492d | ||
|
|
02bcb4d865 | ||
|
|
36962f176f | ||
|
|
0a2afe692f | ||
|
|
5140ee8f2e | ||
|
|
e47ceae0c5 | ||
|
|
906f8c1049 | ||
|
|
27e871656e |
2
.github/config.yml
vendored
2
.github/config.yml
vendored
@@ -1,2 +0,0 @@
|
||||
todo:
|
||||
keyword: "TODO"
|
||||
13
.github/support.yml
vendored
13
.github/support.yml
vendored
@@ -1,13 +0,0 @@
|
||||
# Configuration for support-requests - https://github.com/dessant/support-requests
|
||||
|
||||
# Label used to mark issues as support requests
|
||||
supportLabel: 'Type: Support'
|
||||
# Comment to post on issues marked as support requests. Add a link
|
||||
# to a support page, or set to `false` to disable
|
||||
supportComment: >
|
||||
We use the issue tracker exclusively for bug reports and feature requests.
|
||||
However, this issue appears to be a support request. Please hop over onto our [Discord](https://radarr.video/discord) or [Subreddit](https://reddit.com/r/radarr)
|
||||
# Whether to close issues marked as support requests
|
||||
close: true
|
||||
# Whether to lock issues marked as support requests
|
||||
lock: false
|
||||
21
.github/workflows/support.yml
vendored
Normal file
21
.github/workflows/support.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: 'Support requests'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Type: Support'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://radarr.video/discord)
|
||||
or [Subreddit](https://reddit.com/r/radarr)
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
@@ -7,7 +7,7 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '3.1.1'
|
||||
majorVersion: '3.2.2'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
@@ -23,7 +23,12 @@ trigger:
|
||||
- master
|
||||
|
||||
pr:
|
||||
- develop
|
||||
branches:
|
||||
include:
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
@@ -651,7 +656,7 @@ stages:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'FreeBSD Integration Tests'
|
||||
failTaskOnFailedTests: false
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_Docker
|
||||
|
||||
10
build.sh
10
build.sh
@@ -87,11 +87,11 @@ YarnInstall()
|
||||
ProgressEnd 'yarn install'
|
||||
}
|
||||
|
||||
RunGulp()
|
||||
RunWebpack()
|
||||
{
|
||||
ProgressStart 'Running gulp'
|
||||
yarn run build --production
|
||||
ProgressEnd 'Running gulp'
|
||||
ProgressStart 'Running webpack'
|
||||
yarn run build --env production
|
||||
ProgressEnd 'Running webpack'
|
||||
}
|
||||
|
||||
PackageFiles()
|
||||
@@ -350,7 +350,7 @@ fi
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
then
|
||||
YarnInstall
|
||||
RunGulp
|
||||
RunWebpack
|
||||
fi
|
||||
|
||||
if [ "$LINT" = "YES" ];
|
||||
|
||||
@@ -6,8 +6,10 @@ const dirs = fs
|
||||
.map((dirent) => dirent.name)
|
||||
.join('|');
|
||||
|
||||
const frontendFolder = __dirname;
|
||||
|
||||
module.exports = {
|
||||
parser: 'babel-eslint',
|
||||
parser: '@babel/eslint-parser',
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
@@ -25,6 +27,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
babelOptions: {
|
||||
configFile: `${frontendFolder}/babel.config.js`
|
||||
},
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
impliedStrict: true
|
||||
@@ -271,7 +276,7 @@ module.exports = {
|
||||
|
||||
// ImportSort
|
||||
|
||||
'simple-import-sort/sort': 'error',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'import/newline-after-import': 'error',
|
||||
|
||||
// React
|
||||
@@ -309,7 +314,7 @@ module.exports = {
|
||||
{
|
||||
files: ['*.js'],
|
||||
rules: {
|
||||
'simple-import-sort/sort': [
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
|
||||
269
frontend/build/webpack.config.js
Normal file
269
frontend/build/webpack.config.js
Normal file
@@ -0,0 +1,269 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const LiveReloadPlugin = require('webpack-livereload-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
module.exports = (env) => {
|
||||
const uiFolder = 'UI';
|
||||
const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = !!env.production;
|
||||
const isProfiling = isProduction && !!env.profile;
|
||||
const inlineWebWorkers = 'no-fallback';
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
console.log('Source Folder:', srcFolder);
|
||||
console.log('Output Folder:', distFolder);
|
||||
console.log('isProduction:', isProduction);
|
||||
console.log('isProfiling:', isProfiling);
|
||||
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: 'source-map',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
ignored: /node_modules/
|
||||
},
|
||||
|
||||
entry: {
|
||||
index: 'index.js'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
modules: [
|
||||
srcFolder,
|
||||
path.join(srcFolder, 'Shims'),
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery'
|
||||
},
|
||||
fallback: {
|
||||
buffer: false,
|
||||
http: false,
|
||||
https: false,
|
||||
url: false,
|
||||
util: false,
|
||||
net: false
|
||||
}
|
||||
},
|
||||
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
moduleIds: 'deterministic',
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial',
|
||||
name: 'vendors'
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.html',
|
||||
filename: 'index.html',
|
||||
publicPath: '/'
|
||||
}),
|
||||
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
// HTML
|
||||
{
|
||||
from: 'frontend/src/*.html',
|
||||
to: path.join(distFolder, '[name][ext]'),
|
||||
globOptions: {
|
||||
ignore: ['**/index.html']
|
||||
}
|
||||
},
|
||||
|
||||
// Fonts
|
||||
{
|
||||
from: 'frontend/src/Content/Fonts/*.*',
|
||||
to: path.join(distFolder, 'Content/Fonts', '[name][ext]')
|
||||
},
|
||||
|
||||
// Icon Images
|
||||
{
|
||||
from: 'frontend/src/Content/Images/Icons/*.*',
|
||||
to: path.join(distFolder, 'Content/Images/Icons', '[name][ext]')
|
||||
},
|
||||
|
||||
// Images
|
||||
{
|
||||
from: 'frontend/src/Content/Images/*.*',
|
||||
to: path.join(distFolder, 'Content/Images', '[name][ext]')
|
||||
},
|
||||
|
||||
// Robots
|
||||
{
|
||||
from: 'frontend/src/Content/robots.txt',
|
||||
to: path.join(distFolder, 'Content', '[name][ext]')
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
||||
new LiveReloadPlugin()
|
||||
],
|
||||
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
'node_modules',
|
||||
'frontend/build/webpack/'
|
||||
]
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
filename: '[name].js',
|
||||
inline: inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: `${frontendFolder}/babel.config.js`,
|
||||
envName: isProduction ? 'production' : 'development',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// CSS Modules
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
{ loader: MiniCssExtractPlugin.loader },
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
config: 'frontend/postcss.config.js'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Global styles
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Fonts
|
||||
{
|
||||
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10240,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if (isProfiling) {
|
||||
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
|
||||
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
|
||||
|
||||
config.optimization.minimizer = [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
|
||||
require('./clean');
|
||||
require('./copy');
|
||||
require('./webpack');
|
||||
|
||||
gulp.task('build',
|
||||
gulp.series('clean',
|
||||
gulp.parallel(
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages',
|
||||
'copyRobots'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const del = require('del');
|
||||
|
||||
const paths = require('./helpers/paths');
|
||||
|
||||
gulp.task('clean', () => {
|
||||
return del([paths.dest.root]);
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const print = require('gulp-print').default;
|
||||
const cache = require('gulp-cached');
|
||||
const livereload = require('gulp-livereload');
|
||||
const paths = require('./helpers/paths.js');
|
||||
|
||||
gulp.task('copyHtml', () => {
|
||||
return gulp.src(paths.src.html, { base: paths.src.root })
|
||||
.pipe(cache('copyHtml'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyFonts', () => {
|
||||
return gulp.src(
|
||||
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
|
||||
)
|
||||
.pipe(cache('copyFonts'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyImages', () => {
|
||||
return gulp.src(
|
||||
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
|
||||
)
|
||||
.pipe(cache('copyImages'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyRobots', () => {
|
||||
return gulp.src(paths.src.robots, { base: paths.src.root })
|
||||
.pipe(cache('copyRobots'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
require('./build.js');
|
||||
require('./clean.js');
|
||||
require('./copy.js');
|
||||
require('./watch.js');
|
||||
require('./webpack.js');
|
||||
@@ -1,6 +0,0 @@
|
||||
const colors = require('ansi-colors');
|
||||
|
||||
module.exports = function errorHandler(error) {
|
||||
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
|
||||
this.emit('end');
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
const root = './frontend/src';
|
||||
|
||||
const paths = {
|
||||
src: {
|
||||
root,
|
||||
html: `${root}/*.html`,
|
||||
scripts: `${root}/**/*.js`,
|
||||
content: `${root}/Content/`,
|
||||
fonts: `${root}/Content/Fonts/`,
|
||||
images: `${root}/Content/Images/`,
|
||||
robots: `${root}/Content/robots.txt`,
|
||||
exclude: {
|
||||
libs: `!${root}/JsLibraries/**`
|
||||
}
|
||||
},
|
||||
dest: {
|
||||
root: './_output/UI/',
|
||||
content: './_output/UI/Content/',
|
||||
fonts: './_output/UI/Content/Fonts/',
|
||||
images: './_output/UI/Content/Images/'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = paths;
|
||||
@@ -1,19 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const livereload = require('gulp-livereload');
|
||||
const gulpWatch = require('gulp-watch');
|
||||
const paths = require('./helpers/paths.js');
|
||||
|
||||
require('./copy.js');
|
||||
require('./webpack.js');
|
||||
|
||||
function watch() {
|
||||
livereload.listen({ start: true });
|
||||
|
||||
gulp.task('webpackWatch')();
|
||||
gulpWatch(paths.src.html, gulp.series('copyHtml'));
|
||||
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
|
||||
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
|
||||
gulpWatch(paths.src.robots, gulp.series('copyRobots'));
|
||||
}
|
||||
|
||||
gulp.task('watch', gulp.series('build', watch));
|
||||
@@ -1,275 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const webpackStream = require('webpack-stream');
|
||||
const livereload = require('gulp-livereload');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const errorHandler = require('./helpers/errorHandler');
|
||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
const uiFolder = 'UI';
|
||||
const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = process.argv.indexOf('--production') > -1;
|
||||
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
|
||||
const inlineWebWorkers = 'no-fallback';
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
console.log('Source Folder:', srcFolder);
|
||||
console.log('Output Folder:', distFolder);
|
||||
console.log('isProduction:', isProduction);
|
||||
console.log('isProfiling:', isProfiling);
|
||||
|
||||
const cssVarsFiles = [
|
||||
'../src/Styles/Variables/colors',
|
||||
'../src/Styles/Variables/dimensions',
|
||||
'../src/Styles/Variables/fonts',
|
||||
'../src/Styles/Variables/animations',
|
||||
'../src/Styles/Variables/zIndexes'
|
||||
].map(require.resolve);
|
||||
|
||||
// Override the way HtmlWebpackPlugin injects the scripts
|
||||
// TODO: Find a better way to get these paths without
|
||||
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
||||
const head = assetTags.headTags.map((v) => {
|
||||
const href = v.attributes.href
|
||||
.replace('\\', '/')
|
||||
.replace('%5C', '/');
|
||||
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
const body = assetTags.bodyTags.map((v) => {
|
||||
v.attributes = { src: `/${v.attributes.src}` };
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
|
||||
return html
|
||||
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
|
||||
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /(fetch-cookie|node-fetch|tough-cookie)/
|
||||
}),
|
||||
|
||||
new OptimizeCssAssetsPlugin({}),
|
||||
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: path.join('Content', 'styles.css')
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.html',
|
||||
filename: 'index.html'
|
||||
})
|
||||
];
|
||||
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: '#source-map',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
ignored: /node_modules/
|
||||
},
|
||||
|
||||
entry: {
|
||||
index: 'index.js'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
modules: [
|
||||
srcFolder,
|
||||
path.join(srcFolder, 'Shims'),
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery'
|
||||
}
|
||||
},
|
||||
|
||||
output: {
|
||||
path: distFolder,
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial'
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
plugins,
|
||||
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
'node_modules',
|
||||
'frontend/gulp/webpack/'
|
||||
]
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
filename: '[name].js',
|
||||
inline: inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: `${frontendFolder}/babel.config.js`,
|
||||
envName: isProduction ? 'production' : 'development',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// CSS Modules
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
{ loader: MiniCssExtractPlugin.loader },
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
ident: 'postcss',
|
||||
config: {
|
||||
ctx: {
|
||||
cssVarsFiles
|
||||
},
|
||||
path: 'frontend/postcss.config.js'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Global styles
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Fonts
|
||||
{
|
||||
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10240,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if (isProfiling) {
|
||||
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
|
||||
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
|
||||
|
||||
config.optimization.minimizer = [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
gulp.task('webpack', () => {
|
||||
return webpackStream(config)
|
||||
.pipe(gulp.dest('_output/UI'));
|
||||
});
|
||||
|
||||
gulp.task('webpackWatch', () => {
|
||||
config.watch = true;
|
||||
|
||||
return webpackStream(config, webpack)
|
||||
.on('error', errorHandler)
|
||||
.pipe(gulp.dest('_output/UI'))
|
||||
.on('error', errorHandler)
|
||||
.pipe(livereload())
|
||||
.on('error', errorHandler);
|
||||
});
|
||||
@@ -1,23 +1,32 @@
|
||||
const reload = require('require-nocache')(module);
|
||||
|
||||
module.exports = (ctx, configPath, options) => {
|
||||
const config = {
|
||||
plugins: {
|
||||
'postcss-mixins': {
|
||||
mixinsDir: [
|
||||
'frontend/src/Styles/Mixins'
|
||||
]
|
||||
},
|
||||
'postcss-simple-vars': {
|
||||
variables: () =>
|
||||
ctx.options.cssVarsFiles.reduce((acc, vars) => {
|
||||
return Object.assign(acc, reload(vars));
|
||||
}, {})
|
||||
},
|
||||
'postcss-color-function': {},
|
||||
'postcss-nested': {}
|
||||
}
|
||||
};
|
||||
const cssVarsFiles = [
|
||||
'./src/Styles/Variables/colors',
|
||||
'./src/Styles/Variables/dimensions',
|
||||
'./src/Styles/Variables/fonts',
|
||||
'./src/Styles/Variables/animations',
|
||||
'./src/Styles/Variables/zIndexes'
|
||||
].map(require.resolve);
|
||||
|
||||
return config;
|
||||
};
|
||||
const mixinsFiles = [
|
||||
'frontend/src/Styles/Mixins/cover.css',
|
||||
'frontend/src/Styles/Mixins/linkOverlay.css',
|
||||
'frontend/src/Styles/Mixins/scroller.css',
|
||||
'frontend/src/Styles/Mixins/truncate.css'
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
['postcss-simple-vars', {
|
||||
variables: () =>
|
||||
cssVarsFiles.reduce((acc, vars) => {
|
||||
return Object.assign(acc, reload(vars));
|
||||
}, {})
|
||||
}],
|
||||
'postcss-color-function',
|
||||
'postcss-nested'
|
||||
]
|
||||
};
|
||||
@@ -92,6 +92,19 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.certification {
|
||||
margin-left: 2px;
|
||||
padding: 0 5px;
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.runtime {
|
||||
margin-left: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.statusContainer {
|
||||
margin-right: 22px;
|
||||
font-weight: bold;
|
||||
@@ -103,10 +116,3 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.certification {
|
||||
margin-right: 5px;
|
||||
padding: 0 5px;
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
|
||||
{
|
||||
!!runtime &&
|
||||
<span>
|
||||
<span className={styles.runtime}>
|
||||
{formatRuntime(runtime, movieRuntimeFormat)}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ function createTagListSelector() {
|
||||
(selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
|
||||
filterType !== filterTypes.EQUAL &&
|
||||
filterType !== filterBuilderTypes.NOT_EQUAL ||
|
||||
filterType !== filterTypes.NOT_EQUAL ||
|
||||
!selectedFilterBuilderProp.optionsSelector
|
||||
) {
|
||||
return [];
|
||||
|
||||
@@ -479,6 +479,7 @@ class EnhancedSelectInput extends Component {
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
dividerAfter={v.dividerAfter}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, this.props)}
|
||||
isDisabled={parentSelected}
|
||||
@@ -539,6 +540,7 @@ class EnhancedSelectInput extends Component {
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
dividerAfter={v.dividerAfter}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, this.props)}
|
||||
isMultiSelect={isMultiSelect}
|
||||
|
||||
@@ -13,6 +13,7 @@ import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||
import NumberInput from './NumberInput';
|
||||
import OAuthInputConnector from './OAuthInputConnector';
|
||||
@@ -72,6 +73,9 @@ function getComponent(type) {
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInputConnector;
|
||||
|
||||
case inputTypes.LANGUAGE_SELECT:
|
||||
return LanguageSelectInputConnector;
|
||||
|
||||
case inputTypes.SELECT:
|
||||
return EnhancedSelectInput;
|
||||
|
||||
|
||||
@@ -21,3 +21,8 @@
|
||||
color: $darkGray;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: none;
|
||||
border-bottom: 1px solid $lightGray;
|
||||
}
|
||||
|
||||
@@ -12,37 +12,46 @@ function HintedSelectInputOption(props) {
|
||||
depth,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
dividerAfter,
|
||||
isMultiSelect,
|
||||
isMobile,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
depth={depth}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isHidden={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
isMobile={isMobile}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={classNames(
|
||||
styles.optionText,
|
||||
isMobile && styles.isMobile
|
||||
)}
|
||||
<div>
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
depth={depth}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isHidden={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
isMobile={isMobile}
|
||||
{...otherProps}
|
||||
>
|
||||
<div>{value}</div>
|
||||
<div className={classNames(
|
||||
styles.optionText,
|
||||
isMobile && styles.isMobile
|
||||
)}
|
||||
>
|
||||
<div>{value}</div>
|
||||
|
||||
{
|
||||
hint != null &&
|
||||
<div className={styles.hintText}>
|
||||
{hint}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</EnhancedSelectInputOption>
|
||||
{
|
||||
hint != null &&
|
||||
<div className={styles.hintText}>
|
||||
{hint}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</EnhancedSelectInputOption>
|
||||
|
||||
{
|
||||
dividerAfter ?
|
||||
<div className={styles.divider} /> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,15 +59,18 @@ HintedSelectInputOption.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hint: PropTypes.node,
|
||||
name: PropTypes.string,
|
||||
depth: PropTypes.number,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
dividerAfter: PropTypes.bool.isRequired,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
isMobile: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
HintedSelectInputOption.defaultProps = {
|
||||
isDisabled: false,
|
||||
dividerAfter: false,
|
||||
isHidden: false,
|
||||
isMultiSelect: false
|
||||
};
|
||||
|
||||
52
frontend/src/Components/Form/LanguageSelectInputConnector.js
Normal file
52
frontend/src/Components/Form/LanguageSelectInputConnector.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { values }) => values,
|
||||
( languages ) => {
|
||||
|
||||
const minId = languages.reduce((min, v) => (v.key < 1 ? v.key : min), languages[0].key);
|
||||
|
||||
const values = languages.map(({ key, value }) => {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
dividerAfter: minId < 1 ? key === minId : false
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class LanguageSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LanguageSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(LanguageSelectInputConnector);
|
||||
@@ -49,6 +49,7 @@ function getSelectValues(selectOptions) {
|
||||
result.push({
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
dividerAfter: option.dividerAfter,
|
||||
hint: option.hint
|
||||
});
|
||||
|
||||
|
||||
@@ -38,11 +38,12 @@ class Link extends Component {
|
||||
const linkProps = { target };
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if (to && typeof to === 'string') {
|
||||
if ((/\w+?:\/\//).test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
@@ -52,6 +53,18 @@ class Link extends Component {
|
||||
linkProps.to = `${window.Radarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
} else if (to && typeof to === 'object') {
|
||||
el = RouterLink;
|
||||
linkProps.target = target;
|
||||
if (to.pathname.startsWith(`${window.Radarr.urlBase}/`)) {
|
||||
linkProps.to = to;
|
||||
} else {
|
||||
const pathname = `${window.Radarr.urlBase}/${to.pathname.replace(/^\//, '')}`;
|
||||
linkProps.to = {
|
||||
...to,
|
||||
pathname
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
@@ -82,7 +95,7 @@ class Link extends Component {
|
||||
Link.propTypes = {
|
||||
className: PropTypes.string,
|
||||
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
to: PropTypes.string,
|
||||
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
target: PropTypes.string,
|
||||
isDisabled: PropTypes.bool,
|
||||
noRouter: PropTypes.bool,
|
||||
|
||||
@@ -53,7 +53,13 @@ class PageHeader extends Component {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.logoContainer}>
|
||||
<Link to={'/'}>
|
||||
<Link
|
||||
className={styles.logoLink}
|
||||
to={{
|
||||
pathname: '/',
|
||||
state: { restoreScrollPosition: true }
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={isSmallScreen ? styles.logo : styles.logoFull}
|
||||
src={isSmallScreen ? `${window.Radarr.urlBase}/Content/Images/logo.png` : `${window.Radarr.urlBase}/Content/Images/logo-full.png`}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider } from 'react-dnd-multi-backend';
|
||||
import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -128,7 +128,7 @@ class TableOptionsModal extends Component {
|
||||
const isDraggingDown = isDragging && dropIndex > dragIndex;
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
|
||||
@@ -39,7 +39,8 @@ class VirtualTable extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0
|
||||
width: 0,
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
@@ -48,11 +49,13 @@ class VirtualTable extends Component {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
scrollIndex
|
||||
scrollIndex,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width
|
||||
width,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (this._grid &&
|
||||
@@ -68,6 +71,11 @@ class VirtualTable extends Component {
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -96,6 +104,7 @@ class VirtualTable extends Component {
|
||||
items,
|
||||
scroller,
|
||||
focusScroller,
|
||||
scrollTop: ignored,
|
||||
header,
|
||||
headerHeight,
|
||||
rowRenderer,
|
||||
@@ -180,6 +189,7 @@ VirtualTable.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
scrollIndex: PropTypes.number,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
focusScroller: PropTypes.bool.isRequired,
|
||||
header: PropTypes.node.isRequired,
|
||||
|
||||
@@ -8,7 +8,7 @@ function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
history
|
||||
} = props;
|
||||
|
||||
const scrollTop = history.action === 'POP' ?
|
||||
const scrollTop = history.action === 'POP' || (history.location.state && history.location.state.restoreScrollPosition) ?
|
||||
scrollPositions[scrollPositionKey] :
|
||||
0;
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@ export const PAGE_LAST = fasFastForward;
|
||||
export const PARENT = fasLevelUpAlt;
|
||||
export const PAUSED = fasPause;
|
||||
export const PENDING = farClock;
|
||||
export const PLAY = fasPlay;
|
||||
export const PROFILE = fasUser;
|
||||
export const POSTER = fasTh;
|
||||
export const QUEUED = fasCloud;
|
||||
|
||||
@@ -3,6 +3,7 @@ export const AVAILABILITY_SELECT = 'availabilitySelect';
|
||||
export const CAPTCHA = 'captcha';
|
||||
export const CHECK = 'check';
|
||||
export const DEVICE = 'device';
|
||||
export const KEY_VALUE_LIST = 'keyValueList';
|
||||
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
|
||||
export const NUMBER = 'number';
|
||||
export const OAUTH = 'oauth';
|
||||
@@ -11,6 +12,7 @@ export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||
export const LANGUAGE_SELECT = 'languageSelect';
|
||||
export const SELECT = 'select';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
export const TAG = 'tag';
|
||||
@@ -26,6 +28,7 @@ export const all = [
|
||||
CAPTCHA,
|
||||
CHECK,
|
||||
DEVICE,
|
||||
KEY_VALUE_LIST,
|
||||
MOVIE_MONITORED_SELECT,
|
||||
NUMBER,
|
||||
OAUTH,
|
||||
@@ -34,6 +37,7 @@ export const all = [
|
||||
QUALITY_PROFILE_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
INDEXER_FLAGS_SELECT,
|
||||
LANGUAGE_SELECT,
|
||||
SELECT,
|
||||
DYNAMIC_SELECT,
|
||||
TAG,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const DANGER = 'danger';
|
||||
export const DEFAULT = 'default';
|
||||
export const DELETE = 'delete';
|
||||
export const DISABLED = 'disabled';
|
||||
export const INFO = 'info';
|
||||
export const INVERSE = 'inverse';
|
||||
@@ -13,6 +14,7 @@ export const QUEUE = 'queue';
|
||||
export const all = [
|
||||
DANGER,
|
||||
DEFAULT,
|
||||
DELETE,
|
||||
DISABLED,
|
||||
INFO,
|
||||
INVERSE,
|
||||
|
||||
@@ -180,7 +180,7 @@ class InteractiveSearchRow extends Component {
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
@@ -292,7 +292,7 @@ class InteractiveSearchRow extends Component {
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
@@ -41,6 +41,19 @@ function MovieDetailsLinks(props) {
|
||||
</Label>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://letterboxd.com/tmdb/${tmdbId}`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{translate('Letterboxd')}
|
||||
</Label>
|
||||
</Link>
|
||||
|
||||
{
|
||||
!!imdbId &&
|
||||
<Link
|
||||
@@ -61,7 +74,7 @@ function MovieDetailsLinks(props) {
|
||||
!!imdbId &&
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={` https://moviechat.org/${imdbId}/`}
|
||||
to={`https://moviechat.org/${imdbId}/`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
@@ -77,7 +90,7 @@ function MovieDetailsLinks(props) {
|
||||
!!youTubeTrailerId &&
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={` https://www.youtube.com/watch?v=${youTubeTrailerId}/`}
|
||||
to={`https://www.youtube.com/watch?v=${youTubeTrailerId}/`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
|
||||
@@ -60,7 +60,8 @@ class MovieIndexOverviews extends Component {
|
||||
columnCount: 1,
|
||||
posterWidth: 162,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
@@ -72,13 +73,15 @@ class MovieIndexOverviews extends Component {
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isMovieEditorActive,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
@@ -97,6 +100,11 @@ class MovieIndexOverviews extends Component {
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
@@ -262,6 +270,7 @@ MovieIndexOverviews.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -104,7 +104,8 @@ class MovieIndexPosters extends Component {
|
||||
columnCount: 1,
|
||||
posterWidth: 162,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
@@ -119,14 +120,16 @@ class MovieIndexPosters extends Component {
|
||||
posterOptions,
|
||||
jumpToCharacter,
|
||||
isSmallScreen,
|
||||
isMovieEditorActive
|
||||
isMovieEditorActive,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
@@ -145,6 +148,11 @@ class MovieIndexPosters extends Component {
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
@@ -157,6 +165,10 @@ class MovieIndexPosters extends Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0) {
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -332,6 +344,7 @@ MovieIndexPosters.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
posterOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -87,6 +87,7 @@ class MovieIndexTable extends Component {
|
||||
isSmallScreen,
|
||||
onSortPress,
|
||||
scroller,
|
||||
scrollTop,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
@@ -100,6 +101,7 @@ class MovieIndexTable extends Component {
|
||||
items={items}
|
||||
scrollIndex={this.state.scrollIndex}
|
||||
isSmallScreen={isSmallScreen}
|
||||
scrollTop={scrollTop}
|
||||
scroller={scroller}
|
||||
rowHeight={38}
|
||||
overscanRowCount={2}
|
||||
@@ -130,6 +132,7 @@ MovieIndexTable.propTypes = {
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
jumpToCharacter: PropTypes.string,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -155,7 +155,7 @@ class FileEditModalContent extends Component {
|
||||
<FormLabel>{translate('Languages')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
type={inputTypes.LANGUAGE_SELECT}
|
||||
name="languageIds"
|
||||
value={languageIds}
|
||||
values={languageOptions}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -47,7 +48,8 @@ function EditImportListModalContent(props) {
|
||||
rootFolderPath,
|
||||
searchOnAdd,
|
||||
tags,
|
||||
fields
|
||||
fields,
|
||||
message
|
||||
} = item;
|
||||
|
||||
return (
|
||||
@@ -74,6 +76,15 @@ function EditImportListModalContent(props) {
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
!!message &&
|
||||
<Alert
|
||||
className={styles.message}
|
||||
kind={message.value.type}
|
||||
>
|
||||
{message.value.message}
|
||||
</Alert>
|
||||
}
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ function EditDelayProfileModalContent(props) {
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay,
|
||||
bypassIfHighestQuality,
|
||||
tags
|
||||
} = item;
|
||||
|
||||
@@ -110,6 +111,20 @@ function EditDelayProfileModalContent(props) {
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BypassDelayIfHighestQuality')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="bypassIfHighestQuality"
|
||||
{...bypassIfHighestQuality}
|
||||
helpText={translate('BypassDelayIfHighestQualityHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
id === 1 ?
|
||||
<Alert>
|
||||
|
||||
@@ -254,7 +254,7 @@ class EditQualityProfileModalContent extends Component {
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
type={inputTypes.LANGUAGE_SELECT}
|
||||
name="language"
|
||||
values={languages}
|
||||
value={languageId}
|
||||
|
||||
@@ -96,12 +96,13 @@ function createLanguagesSelector() {
|
||||
(state) => state.settings.languages,
|
||||
(languages) => {
|
||||
const items = languages.items;
|
||||
const filterItems = ['Unknown'];
|
||||
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const newItems = items.map((item) => {
|
||||
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => {
|
||||
return {
|
||||
key: item.id,
|
||||
value: item.name
|
||||
|
||||
@@ -104,7 +104,7 @@ class QualityProfile extends Component {
|
||||
return (
|
||||
<Label
|
||||
key={item.quality.id}
|
||||
kind={isCutoff ? kinds.INFO : kinds.default}
|
||||
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
||||
title={isCutoff ? translate('UpgradeUntilThisQualityIsMetOrExceeded') : null}
|
||||
>
|
||||
{item.quality.name}
|
||||
@@ -120,7 +120,7 @@ class QualityProfile extends Component {
|
||||
className={styles.tooltipLabel}
|
||||
anchor={
|
||||
<Label
|
||||
kind={isCutoff ? kinds.INFO : kinds.default}
|
||||
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
||||
title={isCutoff ? translate('Cutoff') : null}
|
||||
>
|
||||
{item.name}
|
||||
@@ -133,7 +133,7 @@ class QualityProfile extends Component {
|
||||
return (
|
||||
<Label
|
||||
key={groupItem.quality.id}
|
||||
kind={isCutoff ? kinds.INFO : kinds.default}
|
||||
kind={isCutoff ? kinds.INFO : kinds.DEFAULT}
|
||||
title={isCutoff ? translate('Cutoff') : null}
|
||||
>
|
||||
{groupItem.quality.name}
|
||||
|
||||
@@ -200,7 +200,7 @@ class UISettings extends Component {
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MovieInfoLanguage')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
type={inputTypes.LANGUAGE_SELECT}
|
||||
name="movieInfoLanguage"
|
||||
values={languages}
|
||||
helpText={translate('MovieInfoLanguageHelpText')}
|
||||
@@ -213,7 +213,7 @@ class UISettings extends Component {
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('UILanguage')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
type={inputTypes.LANGUAGE_SELECT}
|
||||
name="uiLanguage"
|
||||
values={uiLanguages}
|
||||
helpText={translate('UILanguageHelpText')}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
@define-mixin scrollbarTrack {
|
||||
&&::-webkit-scrollbar-track {
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ function getInternalLink(source) {
|
||||
<IconButton
|
||||
name={icons.PLAY}
|
||||
title={translate('MovieEditor')}
|
||||
to="/movieeditor"
|
||||
to="/"
|
||||
/>
|
||||
);
|
||||
case 'UpdateCheck':
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.queued,
|
||||
.started,
|
||||
.ended {
|
||||
|
||||
@@ -41,7 +41,7 @@ function getStatusIconProps(status, message) {
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.ERROR,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`
|
||||
};
|
||||
|
||||
@@ -157,6 +157,7 @@ class QueuedTaskRow extends Component {
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
clientUserAgent,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onCancelPress
|
||||
@@ -192,7 +193,18 @@ class QueuedTaskRow extends Component {
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>{commandName}</TableRowCell>
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
</span>
|
||||
{
|
||||
clientUserAgent ?
|
||||
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
|
||||
{translate('from')}: {clientUserAgent}
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
@@ -256,6 +268,7 @@ QueuedTaskRow.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
duration: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
clientUserAgent: PropTypes.string,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -226,6 +226,7 @@
|
||||
required
|
||||
title="User name is required"
|
||||
autoFocus="true"
|
||||
autoCapitalize="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
232
package.json
232
package.json
@@ -3,10 +3,11 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Radarr is a PVR for Usenet and BitTorrent users",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"start": "gulp watch",
|
||||
"watch": "gulp watch",
|
||||
"clean": "git clean -fXd",
|
||||
"build": "webpack --config ./frontend/build/webpack.config.js",
|
||||
"prebuild": "yarn clean",
|
||||
"clean": "rimraf ./_output/UI && rimraf \"**/*.js.map\"",
|
||||
"start": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||
"lint": "esprint check",
|
||||
"lint-fix": "esprint check --fix",
|
||||
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
||||
@@ -16,123 +17,120 @@
|
||||
"author": "Team Radarr",
|
||||
"license": "GPL-3.0",
|
||||
"readmeFilename": "readme.md",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.11.6",
|
||||
"@babel/plugin-proposal-class-properties": "7.10.4",
|
||||
"@babel/plugin-proposal-decorators": "7.10.5",
|
||||
"@babel/plugin-proposal-export-default-from": "7.10.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.10.4",
|
||||
"@babel/plugin-proposal-function-sent": "7.10.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.10.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.11.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.10.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.11.5",
|
||||
"@babel/preset-react": "7.10.4",
|
||||
"@fortawesome/fontawesome-free": "5.15.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.31",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.11",
|
||||
"@microsoft/signalr": "5.0.5",
|
||||
"@sentry/browser": "5.29.2",
|
||||
"@sentry/integrations": "5.29.2",
|
||||
"ansi-colors": "4.1.1",
|
||||
"autoprefixer": "9.7.5",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"classnames": "2.2.6",
|
||||
"clipboard": "2.0.6",
|
||||
"connected-react-router": "6.8.0",
|
||||
"core-js": "3.6.5",
|
||||
"css-loader": "3.4.2",
|
||||
"del": "6.0.0",
|
||||
"element-class": "0.2.2",
|
||||
"eslint": "7.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-json": "2.1.2",
|
||||
"eslint-plugin-react": "7.21.3",
|
||||
"eslint-plugin-simple-import-sort": "5.0.3",
|
||||
"esprint": "0.7.0",
|
||||
"file-loader": "6.1.0",
|
||||
"filesize": "6.1.0",
|
||||
"fuse.js": "6.4.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cached": "1.1.1",
|
||||
"gulp-concat": "2.6.1",
|
||||
"gulp-livereload": "4.0.2",
|
||||
"gulp-postcss": "8.0.0",
|
||||
"gulp-print": "5.0.2",
|
||||
"gulp-sourcemaps": "2.6.5",
|
||||
"gulp-watch": "5.0.1",
|
||||
"gulp-wrap": "0.15.0",
|
||||
"history": "4.10.1",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.5.1",
|
||||
"loader-utils": "^2.0.0",
|
||||
"lodash": "4.17.20",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"mobile-detect": "1.4.4",
|
||||
"moment": "2.29.0",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-mixins": "6.2.3",
|
||||
"postcss-nested": "4.2.1",
|
||||
"postcss-simple-vars": "5.0.2",
|
||||
"postcss-url": "8.0.0",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.9.4",
|
||||
"react": "16.13.1",
|
||||
"react-addons-shallow-compare": "15.6.2",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.0.2",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-dnd": "11.1.3",
|
||||
"react-dnd-html5-backend": "11.1.3",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "16.13.1",
|
||||
"react-focus-lock": "2.4.1",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.0.0",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.7",
|
||||
"react-redux": "7.2.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-slider": "1.0.11",
|
||||
"react-tabs": "3.1.1",
|
||||
"react-text-truncate": "0.16.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
"redux": "4.0.5",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"require-nocache": "1.0.0",
|
||||
"reselect": "4.0.0",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "1.2.1",
|
||||
"stylelint": "13.7.2",
|
||||
"stylelint-order": "4.1.0",
|
||||
"url-loader": "4.1.0",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-stream": "6.1.0",
|
||||
"worker-loader": "3.0.3"
|
||||
},
|
||||
"main": "index.js",
|
||||
"browserslist": [
|
||||
">0.25%",
|
||||
"not ie 11",
|
||||
"not op_mini all",
|
||||
"not chrome < 60"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "5.15.3",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@microsoft/signalr": "5.0.5",
|
||||
"@sentry/browser": "6.3.1",
|
||||
"@sentry/integrations": "6.3.1",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.8",
|
||||
"connected-react-router": "6.9.1",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "6.3.0",
|
||||
"fuse.js": "6.4.6",
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.1",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.10.1",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.1.0",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dnd-multi-backend": "6.0.2",
|
||||
"react-dnd-touch-backend": "14.0.0",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.5.0",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.7",
|
||||
"react-redux": "7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-slider": "1.1.4",
|
||||
"react-tabs": "3.2.2",
|
||||
"react-text-truncate": "0.16.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
"redux": "4.1.0",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.13.16",
|
||||
"@babel/plugin-proposal-class-properties": "7.13.0",
|
||||
"@babel/plugin-proposal-decorators": "7.13.15",
|
||||
"@babel/plugin-proposal-export-default-from": "7.12.13",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.12.13",
|
||||
"@babel/plugin-proposal-function-sent": "7.12.13",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.13.8",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.12.13",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.13.12",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.12.13",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.13.15",
|
||||
"@babel/preset-react": "7.13.13",
|
||||
"@babel/eslint-parser": "7.13.14",
|
||||
"autoprefixer": "10.2.5",
|
||||
"babel-loader": "8.2.2",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"copy-webpack-plugin": "8.1.1",
|
||||
"core-js": "3.11.0",
|
||||
"css-loader": "5.2.4",
|
||||
"eslint": "7.25.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-json": "3.0.0",
|
||||
"eslint-plugin-react": "7.23.2",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "2.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
"html-webpack-plugin": "5.3.1",
|
||||
"loader-utils": "^2.0.0",
|
||||
"mini-css-extract-plugin": "1.5.0",
|
||||
"postcss": "8.2.12",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "5.2.0",
|
||||
"postcss-mixins": "7.0.3",
|
||||
"postcss-nested": "5.0.5",
|
||||
"postcss-simple-vars": "6.0.3",
|
||||
"postcss-url": "10.1.3",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "2.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.35.1",
|
||||
"webpack-cli": "4.6.0",
|
||||
"webpack-livereload-plugin": "3.0.1",
|
||||
"worker-loader": "3.0.8",
|
||||
"stylelint": "13.13.0",
|
||||
"stylelint-order": "4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
@@ -21,6 +22,8 @@ namespace NzbDrone.Api.Commands
|
||||
public string Exception { get; set; }
|
||||
public CommandTrigger Trigger { get; set; }
|
||||
|
||||
public string ClientUserAgent { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string CompletionMessage { get; set; }
|
||||
|
||||
@@ -125,6 +128,8 @@ namespace NzbDrone.Api.Commands
|
||||
Exception = model.Exception,
|
||||
Trigger = model.Trigger,
|
||||
|
||||
ClientUserAgent = UserAgentParser.SimplifyUserAgent(model.Body.ClientUserAgent),
|
||||
|
||||
CompletionMessage = model.Body.CompletionMessage,
|
||||
LastExecutionTime = model.Body.LastExecutionTime
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace NzbDrone.Api.Profiles.Delay
|
||||
public DownloadProtocol PreferredProtocol { get; set; }
|
||||
public int UsenetDelay { get; set; }
|
||||
public int TorrentDelay { get; set; }
|
||||
public bool BypassIfHighestQuality { get; set; }
|
||||
public int Order { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
}
|
||||
@@ -35,6 +36,7 @@ namespace NzbDrone.Api.Profiles.Delay
|
||||
PreferredProtocol = model.PreferredProtocol,
|
||||
UsenetDelay = model.UsenetDelay,
|
||||
TorrentDelay = model.TorrentDelay,
|
||||
BypassIfHighestQuality = model.BypassIfHighestQuality,
|
||||
Order = model.Order,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
@@ -56,6 +58,7 @@ namespace NzbDrone.Api.Profiles.Delay
|
||||
PreferredProtocol = resource.PreferredProtocol,
|
||||
UsenetDelay = resource.UsenetDelay,
|
||||
TorrentDelay = resource.TorrentDelay,
|
||||
BypassIfHighestQuality = resource.BypassIfHighestQuality,
|
||||
Order = resource.Order,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Radarr",
|
||||
"description": "Movie Automation",
|
||||
"contact": {
|
||||
"url": "https://radarr.video"
|
||||
},
|
||||
"version": "0.2.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "{protocol}://{hostPath}/api",
|
||||
"variables": {
|
||||
"protocol": {
|
||||
"enum": [
|
||||
"https",
|
||||
"http"
|
||||
],
|
||||
"default": "https"
|
||||
},
|
||||
"hostPath": {
|
||||
"default": "localhost:7878",
|
||||
"description": "Your Radarr Server URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/movie": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Movie"
|
||||
],
|
||||
"summary": "Get all movies",
|
||||
"description": "Returns all movies",
|
||||
"operationId": "getMovie",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Movie"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Movie not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Movie"
|
||||
],
|
||||
"summary": "Add new movie",
|
||||
"requestBody": {
|
||||
"description": "Movie object that needs to be added",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Movie"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"405": {
|
||||
"description": "Validation exception"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Movie"
|
||||
],
|
||||
"summary": "Edit existing movie",
|
||||
"requestBody": {
|
||||
"description": "Movie object that needs to be edited",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Movie"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Movie"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Movie not found"
|
||||
},
|
||||
"405": {
|
||||
"description": "Validation exception"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/movie/{movieId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Movie"
|
||||
],
|
||||
"summary": "Get movie by ID",
|
||||
"description": "Returns a single movie",
|
||||
"operationId": "getMovieById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "movieId",
|
||||
"in": "path",
|
||||
"description": "ID of movie to return",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Movie"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Movie not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Movie"
|
||||
],
|
||||
"summary": "Deletes a Movie",
|
||||
"description": "",
|
||||
"operationId": "deleteMovie",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "movieId",
|
||||
"in": "path",
|
||||
"description": "Movie id to delete",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "addImportExclusion",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "deleteFiles",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Invalid ID supplied"
|
||||
},
|
||||
"404": {
|
||||
"description": "Movie not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Movie": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Dark Phoenix"
|
||||
},
|
||||
"sortTitle": {
|
||||
"type": "string",
|
||||
"example": "dark phoenix"
|
||||
},
|
||||
"sizeOnDisk": {
|
||||
"type": "number"
|
||||
},
|
||||
"overview": {
|
||||
"type": "string"
|
||||
},
|
||||
"inCinemas": {
|
||||
"type": "string"
|
||||
},
|
||||
"physicalRelease": {
|
||||
"type": "string"
|
||||
},
|
||||
"images": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Image"
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"type": "string",
|
||||
"example": "http://darkphoenix.com"
|
||||
},
|
||||
"year": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hasFile": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"youTubeTrailerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"studio": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"qualityProfileId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"monitored": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"minimumAcailability": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"announced",
|
||||
"inCinema",
|
||||
"released"
|
||||
]
|
||||
},
|
||||
"isAvailable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"folderName": {
|
||||
"type": "string"
|
||||
},
|
||||
"runtime": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cleanTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"imdbId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tmdbId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"titleSlug": {
|
||||
"type": "integer"
|
||||
},
|
||||
"certification": {
|
||||
"type": "string"
|
||||
},
|
||||
"genres": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"added": {
|
||||
"type": "string"
|
||||
},
|
||||
"ratings": {
|
||||
"$ref": "#/components/schemas/Rating"
|
||||
},
|
||||
"collection": {
|
||||
"$ref": "#/components/schemas/Collection"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "movie status",
|
||||
"enum": [
|
||||
"deleted",
|
||||
"tba",
|
||||
"announced",
|
||||
"inCinema",
|
||||
"released"
|
||||
]
|
||||
}
|
||||
},
|
||||
"xml": {
|
||||
"name": "Movie"
|
||||
}
|
||||
},
|
||||
"Image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"coverType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"poster",
|
||||
"fanart"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"xml": {
|
||||
"name": "Image"
|
||||
}
|
||||
},
|
||||
"Collection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tmdbId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"images": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Image"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xml": {
|
||||
"name": "Collection"
|
||||
}
|
||||
},
|
||||
"Rating": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"votes": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"xml": {
|
||||
"name": "Rating"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"api_key": {
|
||||
"type": "apiKey",
|
||||
"in": "query",
|
||||
"name": "apiKey"
|
||||
}
|
||||
}
|
||||
},
|
||||
"externalDocs": {
|
||||
"description": "GitHub",
|
||||
"url": "https://github.com/Radarr/Radarr"
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"OutputPath=/home/mySecret/Downloads")]
|
||||
[TestCase("Hardlinking episode file: /home/mySecret/Downloads to /media/abc.mkv")]
|
||||
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||
[TestCase("https://discordnotifier.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
|
||||
// Announce URLs (passkeys) Magnet & Tracker
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||
|
||||
21
src/NzbDrone.Common/Http/UserAgentParser.cs
Normal file
21
src/NzbDrone.Common/Http/UserAgentParser.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
public static class UserAgentParser
|
||||
{
|
||||
public static string SimplifyUserAgent(string userAgent)
|
||||
{
|
||||
if (userAgent == null || userAgent.StartsWith("Mozilla/5.0"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,10 +104,22 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_quality_is_last_allowed_in_profile()
|
||||
public void should_be_false_when_quality_is_last_allowed_in_profile_and_bypass_disabled()
|
||||
{
|
||||
_remoteMovie.ParsedMovieInfo.Quality = new QualityModel(Quality.Bluray720p);
|
||||
|
||||
_remoteMovie.Release.PublishDate = DateTime.UtcNow;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_quality_is_last_allowed_in_profile_and_bypass_enabled()
|
||||
{
|
||||
_delayProfile.BypassIfHighestQuality = true;
|
||||
|
||||
_remoteMovie.ParsedMovieInfo.Quality = new QualityModel(Quality.Bluray720p);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -7,6 +8,7 @@ using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -83,5 +85,123 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||
trackedDownload.RemoteMovie.Movie.Should().NotBeNull();
|
||||
trackedDownload.RemoteMovie.Movie.Id.Should().Be(3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_unmap_tracked_download_if_movie_deleted()
|
||||
{
|
||||
GivenDownloadHistory();
|
||||
|
||||
var remoteMovie = new RemoteMovie
|
||||
{
|
||||
Movie = new Movie() { Id = 3 },
|
||||
|
||||
ParsedMovieInfo = new ParsedMovieInfo()
|
||||
{
|
||||
MovieTitle = "A Movie",
|
||||
Year = 1998
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), null))
|
||||
.Returns(new MappingResult { RemoteMovie = remoteMovie });
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
|
||||
.Returns(new List<MovieHistory>());
|
||||
|
||||
ParseMovieTitle();
|
||||
|
||||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Title = "A Movie 1998",
|
||||
DownloadId = "12345",
|
||||
DownloadClientInfo = new DownloadClientItemClientInfo
|
||||
{
|
||||
Id = 1,
|
||||
Type = "Blackhole",
|
||||
Name = "Blackhole Client",
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
}
|
||||
};
|
||||
|
||||
Subject.TrackDownload(client, item);
|
||||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), null))
|
||||
.Returns(new MappingResult { MappingResultType = MappingResultType.Unknown });
|
||||
|
||||
Subject.Handle(new MoviesDeletedEvent(new List<Movie> { remoteMovie.Movie }, false, false));
|
||||
|
||||
var trackedDownloads = Subject.GetTrackedDownloads();
|
||||
trackedDownloads.Should().HaveCount(1);
|
||||
trackedDownloads.First().RemoteMovie.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_throw_when_processing_deleted_movie()
|
||||
{
|
||||
GivenDownloadHistory();
|
||||
|
||||
var remoteMovie = new RemoteMovie
|
||||
{
|
||||
Movie = new Movie() { Id = 3 },
|
||||
|
||||
ParsedMovieInfo = new ParsedMovieInfo()
|
||||
{
|
||||
MovieTitle = "A Movie",
|
||||
Year = 1998
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), null))
|
||||
.Returns(new MappingResult { MappingResultType = MappingResultType.Unknown });
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
|
||||
.Returns(new List<MovieHistory>());
|
||||
|
||||
ParseMovieTitle();
|
||||
|
||||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Title = "A Movie 1998",
|
||||
DownloadId = "12345",
|
||||
DownloadClientInfo = new DownloadClientItemClientInfo
|
||||
{
|
||||
Id = 1,
|
||||
Type = "Blackhole",
|
||||
Name = "Blackhole Client",
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
}
|
||||
};
|
||||
|
||||
Subject.TrackDownload(client, item);
|
||||
Subject.GetTrackedDownloads().Should().HaveCount(1);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.Map(It.IsAny<ParsedMovieInfo>(), It.IsAny<string>(), null))
|
||||
.Returns(new MappingResult { MappingResultType = MappingResultType.Unknown });
|
||||
|
||||
Subject.Handle(new MoviesDeletedEvent(new List<Movie> { remoteMovie.Movie }, false, false));
|
||||
|
||||
var trackedDownloads = Subject.GetTrackedDownloads();
|
||||
trackedDownloads.Should().HaveCount(1);
|
||||
trackedDownloads.First().RemoteMovie.Should().BeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Processes;
|
||||
using NzbDrone.Core.HealthCheck.Checks;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
@@ -9,6 +11,14 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[TestFixture]
|
||||
public class MonoNotNetCoreCheckFixture : CoreTest<MonoNotNetCoreCheck>
|
||||
{
|
||||
[SetUp]
|
||||
public void setup()
|
||||
{
|
||||
Mocker.GetMock<ILocalizationService>()
|
||||
.Setup(s => s.GetLocalizedString(It.IsAny<string>()))
|
||||
.Returns("Some Warning Message");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Platform(Exclude = "Mono")]
|
||||
public void should_return_ok_if_net_core()
|
||||
@@ -18,14 +28,14 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
|
||||
[Test]
|
||||
[Platform("Mono")]
|
||||
public void should_log_warning_if_mono()
|
||||
public void should_log_error_if_mono()
|
||||
{
|
||||
Subject.Check().ShouldBeWarning();
|
||||
Subject.Check().ShouldBeError();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Platform("Mono")]
|
||||
public void should_return_ok_if_otherbsd()
|
||||
public void should_return_error_if_otherbsd()
|
||||
{
|
||||
Mocker.GetMock<IProcessProvider>()
|
||||
.Setup(x => x.StartAndCapture("uname", null, null))
|
||||
@@ -36,12 +46,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
new ProcessOutputLine(ProcessOutputLevel.Standard, "OpenBSD")
|
||||
}
|
||||
});
|
||||
Subject.Check().ShouldBeOk();
|
||||
Subject.Check().ShouldBeError();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Platform("Mono")]
|
||||
public void should_log_warning_if_freebsd()
|
||||
public void should_log_error_if_freebsd()
|
||||
{
|
||||
Mocker.GetMock<IProcessProvider>()
|
||||
.Setup(x => x.StartAndCapture("uname", null, null))
|
||||
@@ -52,7 +62,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
new ProcessOutputLine(ProcessOutputLevel.Standard, "FreeBSD")
|
||||
}
|
||||
});
|
||||
Subject.Check().ShouldBeWarning();
|
||||
Subject.Check().ShouldBeError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
using NzbDrone.Core.HealthCheck.Checks;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -84,6 +85,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
return false;
|
||||
});
|
||||
|
||||
Mocker.GetMock<ILocalizationService>()
|
||||
.Setup(s => s.GetLocalizedString(It.IsAny<string>()))
|
||||
.Returns("Some Warning Message");
|
||||
}
|
||||
|
||||
private void GivenFolderExists(string folder)
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace NzbDrone.Core.Test.Languages
|
||||
{
|
||||
public static object[] FromIntCases =
|
||||
{
|
||||
new object[] { -2, Language.Original },
|
||||
new object[] { -1, Language.Any },
|
||||
new object[] { 0, Language.Unknown },
|
||||
new object[] { 1, Language.English },
|
||||
new object[] { 2, Language.French },
|
||||
new object[] { 3, Language.Spanish },
|
||||
@@ -45,6 +48,9 @@ namespace NzbDrone.Core.Test.Languages
|
||||
|
||||
public static object[] ToIntCases =
|
||||
{
|
||||
new object[] { Language.Original, -2 },
|
||||
new object[] { Language.Any, -1 },
|
||||
new object[] { Language.Unknown, 0 },
|
||||
new object[] { Language.English, 1 },
|
||||
new object[] { Language.French, 2 },
|
||||
new object[] { Language.Spanish, 3 },
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
|
||||
[TestCase("The Danish Movie 2015")]
|
||||
[TestCase("Movie.Title.2018.2160p.WEBRip.x265.10bit.HDR.DD5.1-GASMASK")]
|
||||
[TestCase("Movie.Title.2010.720p.BluRay.x264.-[YTS.LT]")]
|
||||
public void should_parse_language_unknown(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
private ParsedMovieInfo _translationTitleInfo;
|
||||
private ParsedMovieInfo _umlautInfo;
|
||||
private ParsedMovieInfo _umlautAltInfo;
|
||||
private ParsedMovieInfo _multiLanguageInfo;
|
||||
private ParsedMovieInfo _multiLanguageWithOriginalInfo;
|
||||
private MovieSearchCriteria _movieSearchCriteria;
|
||||
|
||||
[SetUp]
|
||||
@@ -97,6 +99,18 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
Year = _movie.Year
|
||||
};
|
||||
|
||||
_multiLanguageInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitle = _movie.Title,
|
||||
Languages = new List<Language> { Language.Original, Language.French }
|
||||
};
|
||||
|
||||
_multiLanguageWithOriginalInfo = new ParsedMovieInfo
|
||||
{
|
||||
MovieTitle = _movie.Title,
|
||||
Languages = new List<Language> { Language.Original, Language.French, Language.English }
|
||||
};
|
||||
|
||||
_movieSearchCriteria = new MovieSearchCriteria
|
||||
{
|
||||
Movie = _movie
|
||||
@@ -180,5 +194,20 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
Subject.Map(_umlautInfo, "", _movieSearchCriteria).Movie.Should().Be(_movieSearchCriteria.Movie);
|
||||
Subject.Map(_umlautAltInfo, "", _movieSearchCriteria).Movie.Should().Be(_movieSearchCriteria.Movie);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_convert_original()
|
||||
{
|
||||
Subject.Map(_multiLanguageInfo, "", _movieSearchCriteria).RemoteMovie.ParsedMovieInfo.Languages.Should().Contain(Language.English);
|
||||
Subject.Map(_multiLanguageInfo, "", _movieSearchCriteria).RemoteMovie.ParsedMovieInfo.Languages.Should().Contain(Language.French);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_remove_original_as_already_exists()
|
||||
{
|
||||
Subject.Map(_multiLanguageWithOriginalInfo, "", _movieSearchCriteria).RemoteMovie.ParsedMovieInfo.Languages.Should().Contain(Language.English);
|
||||
Subject.Map(_multiLanguageWithOriginalInfo, "", _movieSearchCriteria).RemoteMovie.ParsedMovieInfo.Languages.Should().Contain(Language.French);
|
||||
Subject.Map(_multiLanguageWithOriginalInfo, "", _movieSearchCriteria).RemoteMovie.ParsedMovieInfo.Languages.Should().NotContain(Language.Original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("SomeMovie.1080p.BluRay.DTS.x264.-FTW-HS.mkv", "FTW-HS")]
|
||||
[TestCase("SomeMovie.1080p.BluRay.DTS.x264.-VH-PROD.mkv", "VH-PROD")]
|
||||
[TestCase("Some.Dead.Movie.2006.1080p.BluRay.DTS.x264.D-Z0N3", "D-Z0N3")]
|
||||
[TestCase("Movie.Title.2010.720p.BluRay.x264.-[YTS.LT]", "YTS.LT")]
|
||||
|
||||
//[TestCase("", "")]
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Name { get; set; }
|
||||
public int Order { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public bool DividerAfter { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
@@ -24,12 +24,17 @@ namespace NzbDrone.Core.Authentication
|
||||
private readonly IUserRepository _repo;
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
||||
public UserService(IUserRepository repo,
|
||||
IAppFolderInfo appFolderInfo,
|
||||
IDiskProvider diskProvider,
|
||||
IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_repo = repo;
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public User Add(string username, string password)
|
||||
@@ -105,14 +110,7 @@ namespace NzbDrone.Core.Authentication
|
||||
return;
|
||||
}
|
||||
|
||||
var configFile = _appFolderInfo.GetConfigPath();
|
||||
|
||||
if (!_diskProvider.FileExists(configFile))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var xDoc = XDocument.Load(configFile);
|
||||
var xDoc = _configFileProvider.LoadConfigFile();
|
||||
var config = xDoc.Descendants("Config").Single();
|
||||
var usernameElement = config.Descendants("Username").FirstOrDefault();
|
||||
var passwordElement = config.Descendants("Password").FirstOrDefault();
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace NzbDrone.Core.Configuration
|
||||
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
||||
IExecute<ResetApiKeyCommand>
|
||||
{
|
||||
XDocument LoadConfigFile();
|
||||
Dictionary<string, object> GetConfigDictionary();
|
||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||
|
||||
@@ -310,7 +311,7 @@ namespace NzbDrone.Core.Configuration
|
||||
SaveConfigFile(xDoc);
|
||||
}
|
||||
|
||||
private XDocument LoadConfigFile()
|
||||
public XDocument LoadConfigFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(194)]
|
||||
public class add_bypass_to_delay_profile : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("DelayProfiles").AddColumn("BypassIfHighestQuality").AsBoolean().WithDefaultValue(false);
|
||||
|
||||
// Set to true for existing Delay Profiles to keep behavior the same.
|
||||
Execute.Sql("UPDATE DelayProfiles SET BypassIfHighestQuality = 1;");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(195)]
|
||||
public class update_notifiarr : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql("UPDATE Notifications SET Implementation = Replace(Implementation, 'DiscordNotifier', 'Notifiarr'),ConfigContract = Replace(ConfigContract, 'DiscordNotifierSettings', 'NotifiarrSettings') WHERE Implementation = 'DiscordNotifier';");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
@@ -78,13 +77,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
}
|
||||
|
||||
// If quality meets or exceeds the best allowed quality in the profile accept it immediately
|
||||
var bestQualityInProfile = profile.LastAllowedQuality();
|
||||
var isBestInProfile = comparer.Compare(subject.ParsedMovieInfo.Quality.Quality, bestQualityInProfile) >= 0;
|
||||
|
||||
if (isBestInProfile && isPreferredProtocol)
|
||||
if (delayProfile.BypassIfHighestQuality)
|
||||
{
|
||||
_logger.Debug("Quality is highest in profile for preferred protocol, will not delay.");
|
||||
return Decision.Accept();
|
||||
var bestQualityInProfile = profile.LastAllowedQuality();
|
||||
var isBestInProfile = comparer.Compare(subject.ParsedMovieInfo.Quality.Quality, bestQualityInProfile) >= 0;
|
||||
|
||||
if (isBestInProfile && isPreferredProtocol)
|
||||
{
|
||||
_logger.Debug("Quality is highest in profile for preferred protocol, will not delay.");
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
|
||||
var oldest = _pendingReleaseService.OldestPendingRelease(subject.Movie.Id);
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")]
|
||||
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to NZBGet")]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
|
||||
|
||||
@@ -5,6 +5,11 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class DownloadClientInfo
|
||||
{
|
||||
public DownloadClientInfo()
|
||||
{
|
||||
OutputRootFolders = new List<OsPath>();
|
||||
}
|
||||
|
||||
public bool IsLocalhost { get; set; }
|
||||
public List<OsPath> OutputRootFolders { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Download.History;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Movies.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
@@ -23,7 +24,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
void UpdateTrackable(List<TrackedDownload> trackedDownloads);
|
||||
}
|
||||
|
||||
public class TrackedDownloadService : ITrackedDownloadService
|
||||
public class TrackedDownloadService : ITrackedDownloadService,
|
||||
IHandle<MoviesDeletedEvent>
|
||||
{
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IHistoryService _historyService;
|
||||
@@ -185,6 +187,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCachedItem(TrackedDownload trackedDownload)
|
||||
{
|
||||
var parsedMovieInfo = Parser.Parser.ParseMovieTitle(trackedDownload.DownloadItem.Title);
|
||||
|
||||
trackedDownload.RemoteMovie = parsedMovieInfo == null ? null : _parsingService.Map(parsedMovieInfo, "", null).RemoteMovie;
|
||||
}
|
||||
|
||||
private static TrackedDownloadState GetStateFromHistory(DownloadHistoryEventType eventType)
|
||||
{
|
||||
switch (eventType)
|
||||
@@ -217,5 +226,20 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
downloadItem.OutputPath);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(MoviesDeletedEvent message)
|
||||
{
|
||||
var cachedItems = _cache.Values.Where(t =>
|
||||
t.RemoteMovie?.Movie != null &&
|
||||
message.Movies.Any(m => m.Id == t.RemoteMovie.Movie.Id))
|
||||
.ToList();
|
||||
|
||||
if (cachedItems.Any())
|
||||
{
|
||||
cachedItems.ForEach(UpdateCachedItem);
|
||||
|
||||
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) ||
|
||||
_appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder))
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("AppDataLocationHealthCheckMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("AppDataLocationHealthCheckMessage"), "#updating_will_not_be_possible_to_prevent_deleting_appdata_on_update");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
if (!downloadClients.Any())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("DownloadClientCheckNoneAvailableMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("DownloadClientCheckNoneAvailableMessage"), "#no_download_client_is_available");
|
||||
}
|
||||
|
||||
foreach (var downloadClient in downloadClients)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
@@ -45,14 +46,11 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var status = client.GetStatus();
|
||||
var folders = status.OutputRootFolders;
|
||||
if (folders != null)
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
foreach (var folder in folders)
|
||||
if (rootFolders.Any(r => r.Path.PathEquals(folder.FullPath)))
|
||||
{
|
||||
if (rootFolders.Any(r => r.Path == folder.FullPath))
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot"), client.Definition.Name, folder.FullPath), "#downloads_in_root_folder");
|
||||
}
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientCheckDownloadingToRoot"), client.Definition.Name, folder.FullPath), "#downloads_in_root_folder");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
@@ -39,7 +40,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_diskProvider.FolderExists(rootFolderPath))
|
||||
if (rootFolderPath.IsNullOrWhiteSpace() || !_diskProvider.FolderExists(rootFolderPath))
|
||||
{
|
||||
missingRootFolders.Add(rootFolderPath, new List<ImportListDefinition> { importList });
|
||||
}
|
||||
@@ -50,10 +51,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if (missingRootFolders.Count == 1)
|
||||
{
|
||||
var missingRootFolder = missingRootFolders.First();
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Missing root folder for import list(s): {FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)}", "#import_list_missing_root_folder");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ImportListMissingRoot"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), "#import_list_missing_root_folder");
|
||||
}
|
||||
|
||||
var message = string.Format("Multiple root folders are missing for import lists: {0}", string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))));
|
||||
var message = string.Format(_localizationService.GetLocalizedString("ImportListMultipleMissingRoots"), string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#import_list_missing_root_folder");
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
if (!_configService.EnableCompletedDownloadHandling)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ImportMechanismHealthCheckMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ImportMechanismHealthCheckMessage"), "#completed_download_handling_is_disabled");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -25,14 +25,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
if (enabled.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("IndexerRssHealthCheckNoIndexers"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("IndexerRssHealthCheckNoIndexers"), "#no_indexers_available_with_rss_sync_enabled_radarr_will_not_grab_new_releases_automatically");
|
||||
}
|
||||
|
||||
var active = _indexerFactory.RssEnabled(true);
|
||||
|
||||
if (active.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerRssHealthCheckNoAvailableIndexers"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerRssHealthCheckNoAvailableIndexers"), "#indexers_are_unavailable_due_to_failures");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -25,21 +25,21 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
if (automaticSearchEnabled.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoAutomaticMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoAutomaticMessage"), "#no_indexers_available_with_automatic_search_enabled_radarr_will_not_provide_any_automatic_search_results");
|
||||
}
|
||||
|
||||
var interactiveSearchEnabled = _indexerFactory.InteractiveSearchEnabled(false);
|
||||
|
||||
if (interactiveSearchEnabled.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoInteractiveMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoInteractiveMessage"), "#no_indexers_available_with_interactive_search_enabled");
|
||||
}
|
||||
|
||||
var active = _indexerFactory.AutomaticSearchEnabled(true);
|
||||
|
||||
if (active.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoAvailableIndexersMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoAvailableIndexersMessage"), "#indexers_are_unavailable_due_to_failures");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("MediaInfoDllCheckMessage"), e.Message));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("MediaInfoDllCheckMessage"), e.Message), "#mediainfo_not_loaded");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -28,18 +28,18 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
// Don't warn on linux x86 - we don't build x86 net core
|
||||
if (OsInfo.IsLinux && RuntimeInformation.ProcessArchitecture == Architecture.X86)
|
||||
{
|
||||
return new HealthCheck(GetType());
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("Monox86SupportCheckMessage"), "mono_support_end_of_life");
|
||||
}
|
||||
|
||||
// Check for BSD
|
||||
var output = _processProvider.StartAndCapture("uname");
|
||||
if (output?.ExitCode == 0 && MonoUnames.Contains(output?.Lines.First().Content))
|
||||
{
|
||||
return new HealthCheck(GetType());
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("MonoBSDSupportCheckMessage"), OsInfo.Os), "mono_support_end_of_life");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
HealthCheckResult.Error,
|
||||
_localizationService.GetLocalizedString("MonoNotNetCoreCheckMessage"),
|
||||
"#update_to_net_core_version");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
.WriteSentryDebug("LegacyTlsProvider", monoVersion.ToString())
|
||||
.Write();
|
||||
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("MonoTlsCheckMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("MonoTlsCheckMessage"), "#mono_tls_legacy");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
if (ptpIndexerOldSettings.Any())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("PtpOldSettingsCheckMessage"), string.Join(", ", ptpIndexerOldSettings)));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("PtpOldSettingsCheckMessage"), string.Join(", ", ptpIndexerOldSettings)), "#ptp_settings_old");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
var addresses = Dns.GetHostAddresses(_configService.ProxyHostname);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname), "#proxy_failed_resolve_ip");
|
||||
}
|
||||
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
@@ -51,13 +51,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode), "#proxy_failed_test");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url), "#proxy_failed_test");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,40 +61,37 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
var status = client.GetStatus();
|
||||
var folders = status.OutputRootFolders;
|
||||
if (folders != null)
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
foreach (var folder in folders)
|
||||
if (!folder.IsValid)
|
||||
{
|
||||
if (!folder.IsValid)
|
||||
if (!status.IsLocalhost)
|
||||
{
|
||||
if (!status.IsLocalhost)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#bad_remote_path_mapping");
|
||||
}
|
||||
else if (_osInfo.IsDocker)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#docker_bad_remote_path_mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Local download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your download client settings.", "#bad_download_client_settings");
|
||||
}
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad_remote_path_mapping");
|
||||
}
|
||||
|
||||
if (!_diskProvider.FolderExists(folder.FullPath))
|
||||
else if (_osInfo.IsDocker)
|
||||
{
|
||||
if (_osInfo.IsDocker)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} places downloads in {folder.FullPath} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", "#docker_bad_remote_path_mapping");
|
||||
}
|
||||
else if (!status.IsLocalhost)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} places downloads in {folder.FullPath} but this directory does not appear to exist. Likely missing or incorrect remote path mapping.", "#bad_remote_path_mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Download client {client.Definition.Name} places downloads in {folder.FullPath} but Radarr cannot see this directory. You may need to adjust the folder's permissions.", "#permissions_error");
|
||||
}
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckBadDockerPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#docker_bad_remote_path_mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalWrongOSPath"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad_download_client_settings");
|
||||
}
|
||||
}
|
||||
|
||||
if (!_diskProvider.FolderExists(folder.FullPath))
|
||||
{
|
||||
if (_osInfo.IsDocker)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDockerFolderMissing"), client.Definition.Name, folder.FullPath), "#docker_bad_remote_path_mapping");
|
||||
}
|
||||
else if (!status.IsLocalhost)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckLocalFolderMissing"), client.Definition.Name, folder.FullPath), "#bad_remote_path_mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckGenericPermissions"), client.Definition.Name, folder.FullPath), "#permissions_error");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,13 +127,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
var moviePath = failureMessage.MovieInfo.Path;
|
||||
if (_diskProvider.FileExists(moviePath))
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Radarr can see but not access downloaded movie {moviePath}. Likely permissions error.", "#permissions_error");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckDownloadPermissions"), moviePath), "#permissions_error");
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the file doesn't exist but MovieInfo is not null then the message is coming from
|
||||
// ImportApprovedMovies and the file must have been removed part way through processing
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"File {moviePath} was removed part way though procesing.");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFileRemoved"), moviePath), "#remote_path_file_removed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,43 +149,43 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
// that the user realises something is wrong.
|
||||
if (dlpath.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Radarr failed to import a movie. Check your logs for details.");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("RemotePathMappingCheckImportFailed"), "#remote_path_import_failed");
|
||||
}
|
||||
|
||||
if (!dlpath.IsPathValid())
|
||||
{
|
||||
if (!status.IsLocalhost)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#bad_remote_path_mapping");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad_remote_path_mapping");
|
||||
}
|
||||
else if (_osInfo.IsDocker)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#docker_bad_remote_path_mapping");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesBadDockerPath"), client.Definition.Name, dlpath, _osInfo.Name), "#docker_bad_remote_path_mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Local download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your download client settings.", "#bad_download_client_settings");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesLocalWrongOSPath"), client.Definition.Name, dlpath, _osInfo.Name), "#bad_download_client_settings");
|
||||
}
|
||||
}
|
||||
|
||||
if (_diskProvider.FolderExists(dlpath))
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Radarr can see but not access download directory {dlpath}. Likely permissions error.", "#permissions_error");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFolderPermissions"), dlpath), "#permissions_error");
|
||||
}
|
||||
|
||||
// if it's a remote client/docker, likely missing path mappings
|
||||
if (_osInfo.IsDocker)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} reported files in {dlpath} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", "#docker_bad_remote_path_mapping");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFolderPermissions"), client.Definition.Name, dlpath), "#docker_bad_remote_path_mapping");
|
||||
}
|
||||
else if (!status.IsLocalhost)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} reported files in {dlpath} but this directory does not appear to exist. Likely missing remote path mapping.", "#bad_remote_path_mapping");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckRemoteDownloadClient"), client.Definition.Name, dlpath), "#bad_remote_path_mapping");
|
||||
}
|
||||
else
|
||||
{
|
||||
// path mappings shouldn't be needed locally so probably a permissions issue
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Download client {client.Definition.Name} reported files in {dlpath} but Radarr cannot see this directory. You may need to adjust the folder's permissions.", "#permissions_error");
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingCheckFilesGenericPermissions"), client.Definition.Name, dlpath), "#permissions_error");
|
||||
}
|
||||
}
|
||||
catch (DownloadClientException ex)
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1)
|
||||
{
|
||||
_logger.Error("System time mismatch. SystemTime: {0} Expected Time: {1}. Update system time", systemTime, result.DateTimeUtc);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeCheckMessage"));
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeCheckMessage"), "#system_time_off");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -68,12 +68,9 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14))
|
||||
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14) && _checkUpdateService.AvailableUpdate() != null)
|
||||
{
|
||||
if (_checkUpdateService.AvailableUpdate() != null)
|
||||
{
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, "New update is available");
|
||||
}
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailable"), "#new_update_is_available");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.TMDb.Keyword
|
||||
{
|
||||
public class TMDbKeywordImport : TMDbImportListBase<TMDbKeywordSettings>
|
||||
{
|
||||
public TMDbKeywordImport(IRadarrCloudRequestBuilder requestBuilder,
|
||||
IHttpClient httpClient,
|
||||
IImportListStatusService importListStatusService,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
ISearchForNewMovie searchForNewMovie,
|
||||
Logger logger)
|
||||
: base(requestBuilder, httpClient, importListStatusService, configService, parsingService, searchForNewMovie, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "TMDb Keyword";
|
||||
public override bool Enabled => true;
|
||||
public override bool EnableAuto => false;
|
||||
|
||||
public override IParseImportListResponse GetParser()
|
||||
{
|
||||
return new TMDbKeywordParser();
|
||||
}
|
||||
|
||||
public override IImportListRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new TMDbKeywordRequestGenerator()
|
||||
{
|
||||
RequestBuilder = _requestBuilder,
|
||||
Settings = Settings,
|
||||
Logger = _logger,
|
||||
HttpClient = _httpClient
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.TMDb.Keyword
|
||||
{
|
||||
public class TMDbKeywordParser : TMDbParser
|
||||
{
|
||||
public override IList<ImportListMovie> ParseResponse(ImportListResponse importResponse)
|
||||
{
|
||||
var movies = new List<ImportListMovie>();
|
||||
|
||||
if (!PreProcess(importResponse))
|
||||
{
|
||||
return movies;
|
||||
}
|
||||
|
||||
var jsonResponse = JsonConvert.DeserializeObject<MovieSearchResource>(importResponse.Content);
|
||||
|
||||
// no movies were return
|
||||
if (jsonResponse == null)
|
||||
{
|
||||
return movies;
|
||||
}
|
||||
|
||||
foreach (var movie in jsonResponse.Results)
|
||||
{
|
||||
// Movies with no Year Fix
|
||||
if (string.IsNullOrWhiteSpace(movie.ReleaseDate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
movies.AddIfNotNull(MapListMovie(movie));
|
||||
}
|
||||
|
||||
return movies;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.TMDb.Keyword
|
||||
{
|
||||
public class TMDbKeywordRequestGenerator : IImportListRequestGenerator
|
||||
{
|
||||
public TMDbKeywordSettings Settings { get; set; }
|
||||
public IHttpClient HttpClient { get; set; }
|
||||
public IHttpRequestBuilderFactory RequestBuilder { get; set; }
|
||||
public Logger Logger { get; set; }
|
||||
public int MaxPages { get; set; }
|
||||
|
||||
public TMDbKeywordRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual ImportListPageableRequestChain GetMovies()
|
||||
{
|
||||
var pageableRequests = new ImportListPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetMoviesRequest());
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<ImportListRequest> GetMoviesRequest()
|
||||
{
|
||||
Logger.Info($"Importing TMDb movies from keyword Id: {Settings.KeywordId}");
|
||||
|
||||
var requestBuilder = RequestBuilder.Create()
|
||||
.SetSegment("api", "3")
|
||||
.SetSegment("route", "keyword")
|
||||
.SetSegment("id", $"{Settings.KeywordId}")
|
||||
.SetSegment("secondaryRoute", "/movies");
|
||||
|
||||
var jsonResponse = JsonConvert.DeserializeObject<MovieSearchResource>(HttpClient.Execute(requestBuilder.Build()).Content);
|
||||
|
||||
MaxPages = jsonResponse.TotalPages;
|
||||
|
||||
for (var pageNumber = 1; pageNumber <= MaxPages; pageNumber++)
|
||||
{
|
||||
requestBuilder.AddQueryParam("page", pageNumber, true);
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
|
||||
Logger.Debug($"Importing TMDb movies from: {request.Url}");
|
||||
|
||||
yield return new ImportListRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.TMDb.Keyword
|
||||
{
|
||||
public class TMDbKeywordSettingsValidator : TMDbSettingsBaseValidator<TMDbKeywordSettings>
|
||||
{
|
||||
public TMDbKeywordSettingsValidator()
|
||||
: base()
|
||||
{
|
||||
RuleFor(c => c.KeywordId).Matches(@"^[1-9][0-9]*$", RegexOptions.IgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class TMDbKeywordSettings : TMDbSettingsBase<TMDbKeywordSettings>
|
||||
{
|
||||
protected override AbstractValidator<TMDbKeywordSettings> Validator => new TMDbKeywordSettingsValidator();
|
||||
|
||||
public TMDbKeywordSettings()
|
||||
{
|
||||
KeywordId = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Keyword Id", Type = FieldType.Textbox, HelpText = "TMDb Id of keyword to Follow")]
|
||||
public string KeywordId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -45,13 +45,13 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
[FieldDefinition(1, Label = "Passkey", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
[FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Categories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), Advanced = true, HelpText = "Categories for use in search and feeds. If unspecified, all options are used.")]
|
||||
[FieldDefinition(4, Label = "Categories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), HelpText = "Categories for use in search and feeds. If unspecified, all options are used.")]
|
||||
public IEnumerable<int> Categories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||
[FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Indexers.HDBits
|
||||
[FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Categories", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsCategory), Advanced = true, HelpText = "Options: Movie, TV, Documentary, Music, Sport, Audio, XXX, MiscDemo. If unspecified, all options are used.")]
|
||||
[FieldDefinition(4, Label = "Categories", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsCategory), HelpText = "Options: Movie, TV, Documentary, Music, Sport, Audio, XXX, MiscDemo. If unspecified, all options are used.")]
|
||||
public IEnumerable<int> Categories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Codecs", Type = FieldType.TagSelect, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "Options: h264, Mpeg2, VC1, Xvid. If unspecified, all options are used.")]
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents
|
||||
[FieldDefinition(0, Label = "Feed URL", HelpText = "The full RSS feed url generated by IPTorrents, using only the categories you selected (HD, SD, x264, etc ...)")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
|
||||
@@ -69,13 +69,13 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
|
||||
public string ApiPath { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Drop down list, leave blank to disable all categories", Advanced = true)]
|
||||
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Drop down list, leave blank to disable all categories")]
|
||||
public IEnumerable<int> Categories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Indexers.Nyaa
|
||||
[FieldDefinition(0, Label = "Website URL")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")]
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs
|
||||
[FieldDefinition(2, Label = "Delay", HelpText = "Time in minutes to delay new nzbs before they appear on the RSS feed", Advanced = true)]
|
||||
public int Delay { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
[FieldDefinition(2, Label = "APIKey", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
|
||||
public string APIKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user