commit
0b3a53bfec
@ -1,394 +0,0 @@
|
||||
/* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */
|
||||
function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_ER01_Transcode audio and video with HW (PC and Mac)',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'Transcode Using QSV or VT & FFMPEG',
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: `Files not in H265 will be transcoded into H265 using hw with ffmpeg, assuming mkv container. Plugin uses QS if the node runs on a PC, or Videotoolbox if run on a Mac.
|
||||
Much thanks to Migz for bulk of the important code.
|
||||
Quality is controlled via bitrate adjustments - H264 to H265 assumes 0.5x bitrate. Resolution change from 1080p to 720p assumes 0.7x bitrate.
|
||||
Audio conversion is either 2 channel ac3 or 6 channel ac3, for maximal compatibility and small file size. All subtitles removed.
|
||||
The idea is to homogenize your collection to 1080p or higher movies with 5.1 audio, or 720p TV shows with 2.0 audio.`,
|
||||
|
||||
Tags: 'pre-processing,ffmpeg,video only,configurable,h265',
|
||||
Inputs: [{
|
||||
name: 'audio_channels',
|
||||
tooltip: `Specify whether to modify audio channels.
|
||||
\\n Leave empty to disable.
|
||||
\\nExample:\\n
|
||||
2 - produces single 2.0 channel ac3 audio file, in English, unless not possible.
|
||||
|
||||
\\nExample:\\n
|
||||
6 - produces single 5.1 channel ac3 file, in English, unless not possible.`,
|
||||
},
|
||||
{
|
||||
name: 'resize',
|
||||
tooltip: `Specify if output file should be reduced to 720p from 1080p. Default is false.
|
||||
\\nExample:\\n
|
||||
yes
|
||||
|
||||
\\nExample:\\n
|
||||
no`,
|
||||
},
|
||||
{
|
||||
name: 'bitrate_cutoff',
|
||||
tooltip: `Specify bitrate cutoff, files with a current bitrate lower then this will not be transcoded.
|
||||
\\n Rate is in kbps.
|
||||
\\n Leave empty to disable.
|
||||
\\nExample:\\n
|
||||
6000
|
||||
|
||||
\\nExample:\\n
|
||||
4000`,
|
||||
},
|
||||
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function plugin(file, librarySettings, inputs) {
|
||||
const response = {
|
||||
container: '.mkv',
|
||||
processFile: false,
|
||||
preset: '',
|
||||
handBrakeMode: false,
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: true,
|
||||
infoLog: '',
|
||||
};
|
||||
|
||||
let duration = '';
|
||||
|
||||
let convertAudio = false;
|
||||
let convertVideo = false;
|
||||
let extraArguments = '';
|
||||
|
||||
// Check if inputs.container has been configured. If it hasn't then exit plugin.
|
||||
if (inputs.container === '') {
|
||||
response.infoLog += 'Plugin has not been configured, please configure required options. Skipping this plugin. \n';
|
||||
response.processFile = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check if file is a video. If it isn't then exit plugin.
|
||||
if (file.fileMedium !== 'video') {
|
||||
response.processFile = false;
|
||||
response.infoLog += 'File is not a video. \n';
|
||||
return response;
|
||||
}
|
||||
|
||||
const os = require('os');
|
||||
|
||||
|
||||
// VIDEO SECTION
|
||||
|
||||
let bitRateMultiplier = 1.00;
|
||||
let videoIdx = -1;
|
||||
let willBeResized = false;
|
||||
let videoOptions = `-map 0:v -c:v copy `;
|
||||
|
||||
// video options
|
||||
// hevc, 1080, false - do nothing
|
||||
// hevc, not 1080 - do nothing
|
||||
// hevc, 1080, true - resize, mult 0.5
|
||||
// not hevc, 1080, true - resize, mult 0.25
|
||||
// not hevc, 1080, false - no resize, mult 0.5
|
||||
// not hevc, not 1080 - no resize, mult 0.5
|
||||
|
||||
// Go through each stream in the file.
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
// Check if stream is a video.
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'video') {
|
||||
// Check if codec of stream is mjpeg/png. If so then remove this "video" stream.
|
||||
// mjpeg/png are usually embedded pictures that can cause havoc with plugins.
|
||||
if (file.ffProbeData.streams[i].codec_name === 'mjpeg' || file.ffProbeData.streams[i].codec_name === 'png' ) {
|
||||
extraArguments += `-map -v:${videoIdx} `;
|
||||
convertVideo = true; }
|
||||
/* // no video conversion if: hevc, 1080, false OR hevc, not 1080
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc'
|
||||
&& ((file.video_resolution === '1080p' && inputs.resize === 'no' ) || (file.video_resolution !== '1080p' ))) {
|
||||
convertVideo = false; } */
|
||||
// no video conversion if: hevc, 1080, false
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'no' ) {
|
||||
convertVideo = false; }
|
||||
// no video conversion if: hevc, not 1080
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc' && (file.ffProbeData.streams[i].width < 1800 || file.ffProbeData.streams[i].width > 2000)) {
|
||||
convertVideo = false; }
|
||||
// resize video if: hevc, 1080, true
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'yes' ) {
|
||||
convertVideo = true;
|
||||
willBeResized = true;
|
||||
bitRateMultiplier = 0.7; }
|
||||
// resize video if: not hevc, 1080, true
|
||||
if (file.ffProbeData.streams[i].codec_name !== 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'yes' ) {
|
||||
convertVideo = true;
|
||||
willBeResized = true;
|
||||
bitRateMultiplier = 0.4; }
|
||||
// no resize video if: not hevc, 1080, false
|
||||
if (file.ffProbeData.streams[i].codec_name !== 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'no' ) {
|
||||
convertVideo = true;
|
||||
bitRateMultiplier = 0.5; }
|
||||
// no resize video if: not hevc, not 1080
|
||||
if (file.ffProbeData.streams[i].codec_name !== 'hevc' && file.ffProbeData.streams[i].width < 1800 ) {
|
||||
convertVideo = true;
|
||||
bitRateMultiplier = 0.5; }
|
||||
|
||||
}
|
||||
// Increment videoIdx.
|
||||
videoIdx += 1;
|
||||
}
|
||||
|
||||
// figure out final bitrate
|
||||
// Check if duration info is filled, if so times it by 0.0166667 to get time in minutes.
|
||||
// If not filled then get duration of stream 0 and do the same.
|
||||
if (typeof file.meta.Duration !== 'undefined') {
|
||||
duration = file.meta.Duration * 0.0166667;
|
||||
} else {
|
||||
duration = file.ffProbeData.streams[0].duration * 0.0166667;
|
||||
}
|
||||
|
||||
let bitrateSettings = '';
|
||||
// Work out currentBitrate using "Bitrate = file size / (number of minutes * .0075)"
|
||||
// Used from here https://blog.frame.io/2017/03/06/calculate-video-bitrates/
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const currentBitrate = ~~(file.file_size / (duration * 0.0075));
|
||||
// Use the same calculation used for currentBitrate but divide it in half to get targetBitrate.
|
||||
// Logic of h265 can be half the bitrate as h264 without losing quality.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const targetBitrate = ~~(file.file_size / (duration * 0.0075) * bitRateMultiplier);
|
||||
// Allow some leeway under and over the targetBitrate.
|
||||
const minimumBitrate = ~~(targetBitrate * 0.7);
|
||||
const maximumBitrate = ~~(targetBitrate * 1.3);
|
||||
|
||||
// If targetBitrate comes out as 0 then something has gone wrong and bitrates could not be calculcated.
|
||||
if (targetBitrate === 0) {
|
||||
response.processFile = false;
|
||||
response.infoLog += 'Target bitrate could not be calculated. Skipping this plugin. \n';
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check if inputs.bitrate cutoff has something entered.
|
||||
// (Entered means user actually wants something to happen, empty would disable this).
|
||||
if (inputs.bitrate_cutoff !== '') {
|
||||
// Checks if currentBitrate is below inputs.bitrate_cutoff
|
||||
// If so then don't convert video.
|
||||
if (currentBitrate <= inputs.bitrate_cutoff) {
|
||||
convertVideo = false; }
|
||||
}
|
||||
|
||||
|
||||
// AUDIO SECTION
|
||||
|
||||
// Set up required variables.
|
||||
let audioOptions = `-map 0:a -c:a copy `;
|
||||
let audioIdx = 0;
|
||||
let numberofAudioChannels = 0;
|
||||
let has2Channels = false;
|
||||
let has6Channels = false;
|
||||
let has8Channels = false;
|
||||
let lang2Channels = '';
|
||||
let lang6Channels = '';
|
||||
let lang8Channels = '';
|
||||
let type2Channels = '';
|
||||
let type6Channels = '';
|
||||
let type8Channels = '';
|
||||
|
||||
let keepAudioIdx = -1;
|
||||
let keepIGuessAudioIdx = -1;
|
||||
let encodeAudioIdx = -1;
|
||||
let keepAudioStream = -1;
|
||||
let encodeAudioStream = -1;
|
||||
let originalAudio = '';
|
||||
|
||||
// Go through each stream in the file.
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
// Go through all audio streams and check if 2,6 & 8 channel tracks exist or not.
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'audio') {
|
||||
numberofAudioChannels += 1;
|
||||
if (file.ffProbeData.streams[i].channels === 2 && has2Channels === false) {
|
||||
has2Channels = true;
|
||||
lang2Channels = file.ffProbeData.streams[i].tags.language.toLowerCase();
|
||||
type2Channels = file.ffProbeData.streams[i].codec_name.toLowerCase();
|
||||
}
|
||||
if (file.ffProbeData.streams[i].channels === 6 && has6Channels === false) {
|
||||
has6Channels = true;
|
||||
lang6Channels = file.ffProbeData.streams[i].tags.language.toLowerCase();
|
||||
type6Channels = file.ffProbeData.streams[i].codec_name.toLowerCase();
|
||||
}
|
||||
if (file.ffProbeData.streams[i].channels === 8 && has8Channels === false) {
|
||||
has8Channels = true;
|
||||
lang8Channels = file.ffProbeData.streams[i].tags.language.toLowerCase();
|
||||
type8Channels = file.ffProbeData.streams[i].codec_name.toLowerCase();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Are we processing for 6 channels?
|
||||
if (inputs.audio_channels == 6) {
|
||||
audioIdx = -1;
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'audio') {
|
||||
audioIdx += 1;
|
||||
if (file.ffProbeData.streams[i].tags.language.toLowerCase() === 'eng' || file.ffProbeData.streams[i].tags.language.toLowerCase() === 'und') {
|
||||
if (file.ffProbeData.streams[i].channels == 6 ) {
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'ac3') {
|
||||
//response.infoLog += `Found 6 channel audio in proper language and codec, audio stream ${audioIdx}\n`;
|
||||
if (keepAudioIdx === -1) {
|
||||
keepAudioIdx = audioIdx;
|
||||
keepAudioStream = i;}
|
||||
} else {
|
||||
//response.infoLog += `Found 6 channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
if (encodeAudioIdx === -1) {
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;}
|
||||
}}
|
||||
if (file.ffProbeData.streams[i].channels > 6 ) {
|
||||
//response.infoLog += `Found existing multi-channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
if (encodeAudioIdx === -1) {
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
if (keepAudioIdx === -1 && encodeAudioIdx === -1) { // didn't find any 5.1 or better audio streams in proper language, defaulting to using 2 channels
|
||||
inputs.audio_channels = '2';}
|
||||
}
|
||||
|
||||
|
||||
// Are we processing for 2 channels?
|
||||
if (inputs.audio_channels == 2) {
|
||||
audioIdx = -1;
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'audio') {
|
||||
audioIdx += 1;
|
||||
if (file.ffProbeData.streams[i].tags.language.toLowerCase() === 'eng' || file.ffProbeData.streams[i].tags.language.toLowerCase() === 'und') {
|
||||
if (file.ffProbeData.streams[i].channels == 2 ) {
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'aac' || file.ffProbeData.streams[i].codec_name.toLowerCase() === 'ac3') {
|
||||
//response.infoLog += `Found 2 channel audio in proper language and codec, audio stream ${audioIdx}\n`;
|
||||
if (keepAudioIdx === -1) {
|
||||
keepAudioIdx = audioIdx;
|
||||
keepAudioStream = i;}
|
||||
} else {
|
||||
//response.infoLog += `Found 2 channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
if (encodeAudioIdx === -1) {
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;}
|
||||
}
|
||||
} else {
|
||||
//response.infoLog += `Found existing multi-channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
if (encodeAudioIdx === -1) {
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;}
|
||||
}
|
||||
}
|
||||
// response.infoLog += `a ${audioIdx}. k ${keepAudioIdx}. e ${encodeAudioIdx}\n `;
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let audioMessage = '';
|
||||
|
||||
// selecting channels to keep, only if 2 or 6 channels processed
|
||||
|
||||
if (keepAudioIdx !== -1) {
|
||||
//keep audio, exclude everything else
|
||||
if (numberofAudioChannels !== 1) {
|
||||
convertAudio = true;
|
||||
audioMessage += `keeping audio stream ${keepAudioIdx}.`;
|
||||
audioOptions = `-map 0:a:${keepAudioIdx} -c:a copy `;
|
||||
originalAudio += `${file.ffProbeData.streams[keepAudioStream].channels} channel ${file.ffProbeData.streams[keepAudioStream].codec_name} --> ${inputs.audio_channels} channel ac3`;}
|
||||
} else {
|
||||
if (encodeAudioIdx !== -1) {
|
||||
// encode this audio
|
||||
convertAudio = true;
|
||||
audioMessage += `encoding audio stream ${encodeAudioIdx}. `;
|
||||
audioOptions = `-map 0:a:${encodeAudioIdx} -c:a ac3 -ac ${inputs.audio_channels} `; // 2 or 6 channels encoding
|
||||
originalAudio += `${file.ffProbeData.streams[encodeAudioStream].channels} channel ${file.ffProbeData.streams[encodeAudioStream].codec_name} --> ${inputs.audio_channels} channel ac3`;
|
||||
} else {
|
||||
// do not encode audio
|
||||
convertAudio = false;
|
||||
audioMessage += `no audio to encode.`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// test for whether the file needs to be processed - separate for video and audio convertAudio, convertVideo
|
||||
|
||||
if (convertAudio === false && convertVideo === false) { // if nothing to do, exit
|
||||
response.infoLog += `File is processed already, nothing to do`;
|
||||
response.processFile = false;
|
||||
return response; }
|
||||
|
||||
// Generate ffmpeg command line arguments in total
|
||||
|
||||
// few defaults
|
||||
|
||||
response.preset = `, -sn `;
|
||||
|
||||
|
||||
if (convertVideo === true) {
|
||||
// Set bitrateSettings variable using bitrate information calculated earlier.
|
||||
bitrateSettings = `-b:v ${targetBitrate}k -minrate ${minimumBitrate}k `
|
||||
+ `-maxrate ${maximumBitrate}k -bufsize ${currentBitrate}k`;
|
||||
|
||||
if (willBeResized === true) {
|
||||
extraArguments += `-filter:v scale=1280:-1 `; }
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
videoOptions = `-map 0:v -c:v hevc_videotoolbox -profile main `;
|
||||
}
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
videoOptions = `-map 0:v -c:v hevc_qsv -load_plugin hevc_hw `;
|
||||
}
|
||||
}
|
||||
|
||||
response.preset += `${videoOptions} ${bitrateSettings} ${extraArguments} ${audioOptions} `;
|
||||
|
||||
|
||||
let outputResolution = file.video_resolution;
|
||||
if (willBeResized === true) {
|
||||
outputResolution = '720p';}
|
||||
|
||||
if (convertVideo === false) {
|
||||
response.infoLog += `NOT converting video ${file.video_resolution}, ${file.video_codec_name}, bitrate = ${currentBitrate} \n`;
|
||||
} else {
|
||||
response.infoLog += `Converting video, `;
|
||||
if (willBeResized === false ) { response.infoLog += `NOT `; }
|
||||
response.infoLog += `resizing. ${file.video_resolution}, ${file.video_codec_name} --> ${outputResolution}, hevc. bitrate = ${currentBitrate} --> ${targetBitrate}, multiplier ${bitRateMultiplier}. \n`;
|
||||
}
|
||||
|
||||
if (convertAudio === true) {
|
||||
response.infoLog += `Converting audio, ${audioMessage} ${originalAudio}. \n`;
|
||||
} else {
|
||||
response.infoLog += `Not converting audio. \n`;}
|
||||
|
||||
response.infoLog += `2 channels - ${lang2Channels} ${type2Channels} \n`;
|
||||
response.infoLog += `6 channels - ${lang6Channels} ${type6Channels} \n`;
|
||||
response.infoLog += `8 channels - ${lang8Channels} ${type8Channels} `;
|
||||
|
||||
response.processFile = true;
|
||||
return response;
|
||||
}
|
||||
module.exports.details = details;
|
||||
module.exports.plugin = plugin;
|
||||
@ -0,0 +1,400 @@
|
||||
/* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */
|
||||
/* eslint max-len: 0 */
|
||||
/* eslint no-bitwise: 0 */
|
||||
/* eslint no-mixed-operators: 0 */
|
||||
|
||||
const os = require('os');
|
||||
|
||||
function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_ER01_Transcode audio and video with HW (PC and Mac)',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'Transcode Using QSV or VT & FFMPEG',
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: `Files not in H265 will be transcoded into H265 using hw with ffmpeg, assuming mkv container. Plugin uses QS if the node runs on a PC, or Videotoolbox if run on a Mac.
|
||||
Much thanks to Migz for bulk of the important code.
|
||||
Quality is controlled via bitrate adjustments - H264 to H265 assumes 0.5x bitrate. Resolution change from 1080p to 720p assumes 0.7x bitrate.
|
||||
Audio conversion is either 2 channel ac3 or 6 channel ac3, for maximal compatibility and small file size. All subtitles removed.
|
||||
The idea is to homogenize your collection to 1080p or higher movies with 5.1 audio, or 720p TV shows with 2.0 audio.`,
|
||||
|
||||
Tags: 'pre-processing,ffmpeg,video only,configurable,h265',
|
||||
Inputs: [{
|
||||
name: 'audio_channels',
|
||||
tooltip: `Specify whether to modify audio channels.
|
||||
\\n Leave empty to disable.
|
||||
\\nExample:\\n
|
||||
2 - produces single 2.0 channel ac3 audio file, in English, unless not possible.
|
||||
|
||||
\\nExample:\\n
|
||||
6 - produces single 5.1 channel ac3 file, in English, unless not possible.`,
|
||||
},
|
||||
{
|
||||
name: 'resize',
|
||||
tooltip: `Specify if output file should be reduced to 720p from 1080p. Default is false.
|
||||
\\nExample:\\n
|
||||
yes
|
||||
|
||||
\\nExample:\\n
|
||||
no`,
|
||||
},
|
||||
{
|
||||
name: 'bitrate_cutoff',
|
||||
tooltip: `Specify bitrate cutoff, files with a current bitrate lower then this will not be transcoded.
|
||||
\\n Rate is in kbps.
|
||||
\\n Leave empty to disable.
|
||||
\\nExample:\\n
|
||||
6000
|
||||
|
||||
\\nExample:\\n
|
||||
4000`,
|
||||
},
|
||||
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function plugin(file, librarySettings, inputs) {
|
||||
const response = {
|
||||
container: '.mkv',
|
||||
processFile: false,
|
||||
preset: '',
|
||||
handBrakeMode: false,
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: true,
|
||||
infoLog: '',
|
||||
};
|
||||
|
||||
let duration = '';
|
||||
|
||||
let convertAudio = false;
|
||||
let convertVideo = false;
|
||||
let extraArguments = '';
|
||||
|
||||
// Check if inputs.container has been configured. If it hasn't then exit plugin.
|
||||
if (inputs.container === '') {
|
||||
response.infoLog += 'Plugin has not been configured, please configure required options. Skipping this plugin. \n';
|
||||
response.processFile = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check if file is a video. If it isn't then exit plugin.
|
||||
if (file.fileMedium !== 'video') {
|
||||
response.processFile = false;
|
||||
response.infoLog += 'File is not a video. \n';
|
||||
return response;
|
||||
}
|
||||
|
||||
// VIDEO SECTION
|
||||
|
||||
let bitRateMultiplier = 1.00;
|
||||
let videoIdx = -1;
|
||||
let willBeResized = false;
|
||||
let videoOptions = '-map 0:v -c:v copy ';
|
||||
|
||||
// video options
|
||||
// hevc, 1080, false - do nothing
|
||||
// hevc, not 1080 - do nothing
|
||||
// hevc, 1080, true - resize, mult 0.5
|
||||
// not hevc, 1080, true - resize, mult 0.25
|
||||
// not hevc, 1080, false - no resize, mult 0.5
|
||||
// not hevc, not 1080 - no resize, mult 0.5
|
||||
|
||||
// Go through each stream in the file.
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
// Check if stream is a video.
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'video') {
|
||||
// Check if codec of stream is mjpeg/png. If so then remove this "video" stream.
|
||||
// mjpeg/png are usually embedded pictures that can cause havoc with plugins.
|
||||
if (file.ffProbeData.streams[i].codec_name === 'mjpeg' || file.ffProbeData.streams[i].codec_name === 'png') {
|
||||
extraArguments += `-map -v:${videoIdx} `;
|
||||
convertVideo = true;
|
||||
}
|
||||
/* // no video conversion if: hevc, 1080, false OR hevc, not 1080
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc'
|
||||
&& ((file.video_resolution === '1080p' && inputs.resize === 'no' ) || (file.video_resolution !== '1080p' ))) {
|
||||
convertVideo = false; } */
|
||||
// no video conversion if: hevc, 1080, false
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'no') {
|
||||
convertVideo = false;
|
||||
}
|
||||
// no video conversion if: hevc, not 1080
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc' && (file.ffProbeData.streams[i].width < 1800 || file.ffProbeData.streams[i].width > 2000)) {
|
||||
convertVideo = false;
|
||||
}
|
||||
// resize video if: hevc, 1080, true
|
||||
if (file.ffProbeData.streams[i].codec_name === 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'yes') {
|
||||
convertVideo = true;
|
||||
willBeResized = true;
|
||||
bitRateMultiplier = 0.7;
|
||||
}
|
||||
// resize video if: not hevc, 1080, true
|
||||
if (file.ffProbeData.streams[i].codec_name !== 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'yes') {
|
||||
convertVideo = true;
|
||||
willBeResized = true;
|
||||
bitRateMultiplier = 0.4;
|
||||
}
|
||||
// no resize video if: not hevc, 1080, false
|
||||
if (file.ffProbeData.streams[i].codec_name !== 'hevc' && file.ffProbeData.streams[i].width > 1800 && file.ffProbeData.streams[i].width < 2000 && inputs.resize === 'no') {
|
||||
convertVideo = true;
|
||||
bitRateMultiplier = 0.5;
|
||||
}
|
||||
// no resize video if: not hevc, not 1080
|
||||
if (file.ffProbeData.streams[i].codec_name !== 'hevc' && file.ffProbeData.streams[i].width < 1800) {
|
||||
convertVideo = true;
|
||||
bitRateMultiplier = 0.5;
|
||||
}
|
||||
}
|
||||
// Increment videoIdx.
|
||||
videoIdx += 1;
|
||||
}
|
||||
|
||||
// figure out final bitrate
|
||||
// Check if duration info is filled, if so times it by 0.0166667 to get time in minutes.
|
||||
// If not filled then get duration of stream 0 and do the same.
|
||||
if (typeof file.meta.Duration !== 'undefined') {
|
||||
duration = file.meta.Duration * 0.0166667;
|
||||
} else {
|
||||
duration = file.ffProbeData.streams[0].duration * 0.0166667;
|
||||
}
|
||||
|
||||
let bitrateSettings = '';
|
||||
// Work out currentBitrate using "Bitrate = file size / (number of minutes * .0075)"
|
||||
// Used from here https://blog.frame.io/2017/03/06/calculate-video-bitrates/
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const currentBitrate = ~~(file.file_size / (duration * 0.0075));
|
||||
// Use the same calculation used for currentBitrate but divide it in half to get targetBitrate.
|
||||
// Logic of h265 can be half the bitrate as h264 without losing quality.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const targetBitrate = ~~(file.file_size / (duration * 0.0075) * bitRateMultiplier);
|
||||
// Allow some leeway under and over the targetBitrate.
|
||||
const minimumBitrate = ~~(targetBitrate * 0.7);
|
||||
const maximumBitrate = ~~(targetBitrate * 1.3);
|
||||
|
||||
// If targetBitrate comes out as 0 then something has gone wrong and bitrates could not be calculcated.
|
||||
if (targetBitrate === 0) {
|
||||
response.processFile = false;
|
||||
response.infoLog += 'Target bitrate could not be calculated. Skipping this plugin. \n';
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check if inputs.bitrate cutoff has something entered.
|
||||
// (Entered means user actually wants something to happen, empty would disable this).
|
||||
if (inputs.bitrate_cutoff !== '') {
|
||||
// Checks if currentBitrate is below inputs.bitrate_cutoff
|
||||
// If so then don't convert video.
|
||||
if (currentBitrate <= inputs.bitrate_cutoff) {
|
||||
convertVideo = false;
|
||||
}
|
||||
}
|
||||
|
||||
// AUDIO SECTION
|
||||
|
||||
// Set up required variables.
|
||||
let audioOptions = '-map 0:a -c:a copy ';
|
||||
let audioIdx = 0;
|
||||
let numberofAudioChannels = 0;
|
||||
let has2Channels = false;
|
||||
let has6Channels = false;
|
||||
let has8Channels = false;
|
||||
let lang2Channels = '';
|
||||
let lang6Channels = '';
|
||||
let lang8Channels = '';
|
||||
let type2Channels = '';
|
||||
let type6Channels = '';
|
||||
let type8Channels = '';
|
||||
|
||||
let keepAudioIdx = -1;
|
||||
// const keepIGuessAudioIdx = -1;
|
||||
let encodeAudioIdx = -1;
|
||||
let keepAudioStream = -1;
|
||||
let encodeAudioStream = -1;
|
||||
let originalAudio = '';
|
||||
|
||||
// Go through each stream in the file.
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
// Go through all audio streams and check if 2,6 & 8 channel tracks exist or not.
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'audio') {
|
||||
numberofAudioChannels += 1;
|
||||
if (file.ffProbeData.streams[i].channels === 2 && has2Channels === false) {
|
||||
has2Channels = true;
|
||||
lang2Channels = file.ffProbeData.streams[i].tags.language.toLowerCase();
|
||||
type2Channels = file.ffProbeData.streams[i].codec_name.toLowerCase();
|
||||
}
|
||||
if (file.ffProbeData.streams[i].channels === 6 && has6Channels === false) {
|
||||
has6Channels = true;
|
||||
lang6Channels = file.ffProbeData.streams[i].tags.language.toLowerCase();
|
||||
type6Channels = file.ffProbeData.streams[i].codec_name.toLowerCase();
|
||||
}
|
||||
if (file.ffProbeData.streams[i].channels === 8 && has8Channels === false) {
|
||||
has8Channels = true;
|
||||
lang8Channels = file.ffProbeData.streams[i].tags.language.toLowerCase();
|
||||
type8Channels = file.ffProbeData.streams[i].codec_name.toLowerCase();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
|
||||
// Are we processing for 6 channels?
|
||||
if (inputs.audio_channels === 6) {
|
||||
audioIdx = -1;
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'audio') {
|
||||
audioIdx += 1;
|
||||
if (file.ffProbeData.streams[i].tags.language.toLowerCase() === 'eng' || file.ffProbeData.streams[i].tags.language.toLowerCase() === 'und') {
|
||||
if (file.ffProbeData.streams[i].channels === 6) {
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'ac3') {
|
||||
// response.infoLog += `Found 6 channel audio in proper language and codec, audio stream ${audioIdx}\n`;
|
||||
if (keepAudioIdx === -1) {
|
||||
keepAudioIdx = audioIdx;
|
||||
keepAudioStream = i;
|
||||
}
|
||||
} else if (encodeAudioIdx === -1) {
|
||||
// response.infoLog += `Found 6 channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;
|
||||
}
|
||||
}
|
||||
if (file.ffProbeData.streams[i].channels > 6) {
|
||||
// response.infoLog += `Found existing multi-channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
if (encodeAudioIdx === -1) {
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
if (keepAudioIdx === -1 && encodeAudioIdx === -1) { // didn't find any 5.1 or better audio streams in proper language, defaulting to using 2 channels
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
inputs.audio_channels = '2';
|
||||
}
|
||||
}
|
||||
|
||||
// Are we processing for 2 channels?
|
||||
if (inputs.audio_channels === 2) {
|
||||
audioIdx = -1;
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'audio') {
|
||||
audioIdx += 1;
|
||||
if (file.ffProbeData.streams[i].tags.language.toLowerCase() === 'eng' || file.ffProbeData.streams[i].tags.language.toLowerCase() === 'und') {
|
||||
if (file.ffProbeData.streams[i].channels === 2) {
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'aac' || file.ffProbeData.streams[i].codec_name.toLowerCase() === 'ac3') {
|
||||
// response.infoLog += `Found 2 channel audio in proper language and codec, audio stream ${audioIdx}\n`;
|
||||
if (keepAudioIdx === -1) {
|
||||
keepAudioIdx = audioIdx;
|
||||
keepAudioStream = i;
|
||||
}
|
||||
} else if (encodeAudioIdx === -1) {
|
||||
// response.infoLog += `Found 2 channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;
|
||||
}
|
||||
} else if (encodeAudioIdx === -1) {
|
||||
// response.infoLog += `Found existing multi-channel audio in proper language, need to re-encode, audio stream ${audioIdx}\n`;
|
||||
encodeAudioIdx = audioIdx;
|
||||
encodeAudioStream = i;
|
||||
}
|
||||
}
|
||||
// response.infoLog += `a ${audioIdx}. k ${keepAudioIdx}. e ${encodeAudioIdx}\n `;
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let audioMessage = '';
|
||||
|
||||
// selecting channels to keep, only if 2 or 6 channels processed
|
||||
|
||||
if (keepAudioIdx !== -1) {
|
||||
// keep audio, exclude everything else
|
||||
if (numberofAudioChannels !== 1) {
|
||||
convertAudio = true;
|
||||
audioMessage += `keeping audio stream ${keepAudioIdx}.`;
|
||||
audioOptions = `-map 0:a:${keepAudioIdx} -c:a copy `;
|
||||
originalAudio += `${file.ffProbeData.streams[keepAudioStream].channels} channel ${file.ffProbeData.streams[keepAudioStream].codec_name} --> ${inputs.audio_channels} channel ac3`;
|
||||
}
|
||||
} else if (encodeAudioIdx !== -1) {
|
||||
// encode this audio
|
||||
convertAudio = true;
|
||||
audioMessage += `encoding audio stream ${encodeAudioIdx}. `;
|
||||
audioOptions = `-map 0:a:${encodeAudioIdx} -c:a ac3 -ac ${inputs.audio_channels} `; // 2 or 6 channels encoding
|
||||
originalAudio += `${file.ffProbeData.streams[encodeAudioStream].channels} channel ${file.ffProbeData.streams[encodeAudioStream].codec_name} --> ${inputs.audio_channels} channel ac3`;
|
||||
} else {
|
||||
// do not encode audio
|
||||
convertAudio = false;
|
||||
audioMessage += 'no audio to encode.';
|
||||
}
|
||||
|
||||
// test for whether the file needs to be processed - separate for video and audio convertAudio, convertVideo
|
||||
|
||||
if (convertAudio === false && convertVideo === false) { // if nothing to do, exit
|
||||
response.infoLog += 'File is processed already, nothing to do';
|
||||
response.processFile = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
// Generate ffmpeg command line arguments in total
|
||||
|
||||
// few defaults
|
||||
|
||||
response.preset = ', -sn ';
|
||||
|
||||
if (convertVideo === true) {
|
||||
// Set bitrateSettings variable using bitrate information calculated earlier.
|
||||
bitrateSettings = `-b:v ${targetBitrate}k -minrate ${minimumBitrate}k `
|
||||
+ `-maxrate ${maximumBitrate}k -bufsize ${currentBitrate}k`;
|
||||
|
||||
if (willBeResized === true) {
|
||||
extraArguments += '-filter:v scale=1280:-1 ';
|
||||
}
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
videoOptions = '-map 0:v -c:v hevc_videotoolbox -profile main ';
|
||||
}
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
videoOptions = '-map 0:v -c:v hevc_qsv -load_plugin hevc_hw ';
|
||||
}
|
||||
}
|
||||
|
||||
response.preset += `${videoOptions} ${bitrateSettings} ${extraArguments} ${audioOptions} `;
|
||||
|
||||
let outputResolution = file.video_resolution;
|
||||
if (willBeResized === true) {
|
||||
outputResolution = '720p';
|
||||
}
|
||||
|
||||
if (convertVideo === false) {
|
||||
response.infoLog += `NOT converting video ${file.video_resolution}, ${file.video_codec_name}, bitrate = ${currentBitrate} \n`;
|
||||
} else {
|
||||
response.infoLog += 'Converting video, ';
|
||||
if (willBeResized === false) { response.infoLog += 'NOT '; }
|
||||
response.infoLog += `resizing. ${file.video_resolution}, ${file.video_codec_name} --> ${outputResolution}, hevc. bitrate = ${currentBitrate} --> ${targetBitrate}, multiplier ${bitRateMultiplier}. \n`;
|
||||
}
|
||||
|
||||
if (convertAudio === true) {
|
||||
response.infoLog += `Converting audio, ${audioMessage} ${originalAudio}. \n`;
|
||||
} else {
|
||||
response.infoLog += 'Not converting audio. \n';
|
||||
}
|
||||
|
||||
response.infoLog += `2 channels - ${lang2Channels} ${type2Channels} \n`;
|
||||
response.infoLog += `6 channels - ${lang6Channels} ${type6Channels} \n`;
|
||||
response.infoLog += `8 channels - ${lang8Channels} ${type8Channels} `;
|
||||
|
||||
response.processFile = true;
|
||||
return response;
|
||||
}
|
||||
module.exports.details = details;
|
||||
module.exports.plugin = plugin;
|
||||
@ -0,0 +1,220 @@
|
||||
/* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */
|
||||
// This is almost a line for line copy of Migz1FFMPEG
|
||||
// https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/Tdarr_Plugin_MC93_Migz1FFMPEG.js
|
||||
// Seriously, all I did was make it work for converting things to h264 instead of hevc
|
||||
|
||||
module.exports.details = function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_SV6x_Smoove1FFMPEG_NVENC_H264',
|
||||
Stage: 'Pre-processing', // Preprocessing or Post-processing. Determines when the plugin will be executed.
|
||||
Name: 'Smoove-Transcode to H264 using FFMPEG and NVENC ',
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: `Files not in H264 will be transcoded into H264 using Nvidia GPU with ffmpeg.
|
||||
Settings are dependant on file bitrate
|
||||
NVDEC & NVENC compatable GPU required.`,
|
||||
Version: '1.00',
|
||||
Link: `https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/
|
||||
Tdarr_Plugin_SV6x_Smoove1FFMPEG_NVENC_H264.js`,
|
||||
Tags: 'pre-processing,ffmpeg,video only,nvenc h264,configurable',
|
||||
// Provide tags to categorise your plugin in the plugin browser.Tag options: h265,hevc,h264,nvenc h265,
|
||||
// nvenc h264,video only,audio only,subtitle only,handbrake,ffmpeg
|
||||
// radarr,sonarr,pre-processing,post-processing,configurable
|
||||
|
||||
Inputs: [
|
||||
{
|
||||
name: 'container',
|
||||
tooltip: `Specify output container of file
|
||||
\\n Ensure that all stream types you may have are supported by your chosen container.
|
||||
\\n mkv is recommended.
|
||||
\\nExample:\\n
|
||||
mkv
|
||||
|
||||
\\nExample:\\n
|
||||
mp4`,
|
||||
},
|
||||
{
|
||||
name: 'force_conform',
|
||||
tooltip: `Make the file conform to output containers requirements.
|
||||
\\n Drop hdmv_pgs_subtitle/eia_608/subrip/timed_id3 for MP4.
|
||||
\\n Drop data streams/mov_text/eia_608/timed_id3 for MKV.
|
||||
\\n Default is false.
|
||||
\\nExample:\\n
|
||||
true
|
||||
|
||||
\\nExample:\\n
|
||||
false`,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.plugin = function plugin(file, librarySettings, inputs) {
|
||||
const response = {
|
||||
processFile: false,
|
||||
infoLog: '',
|
||||
handBrakeMode: false, // Set whether to use HandBrake or FFmpeg for transcoding
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: true,
|
||||
// Leave as true. File will be re-qeued afterwards and pass through the plugin
|
||||
// filter again to make sure it meets conditions.
|
||||
};
|
||||
|
||||
// Check that inputs.container has been configured, else dump out
|
||||
if (inputs.container === '') {
|
||||
response.infoLog += 'Plugin has not been configured, please configure required options. Skipping this plugin. \n';
|
||||
response.processFile = false;
|
||||
return response;
|
||||
}
|
||||
response.container = `.${inputs.container}`;
|
||||
|
||||
// Check if file is a video. If it isn't then exit plugin.
|
||||
if (file.fileMedium !== 'video') {
|
||||
response.processFile = false;
|
||||
response.infoLog += 'File is not a video. \n';
|
||||
return response;
|
||||
}
|
||||
|
||||
let duration = '';
|
||||
|
||||
// Get duration of stream 0 and times it by 0.0166667 to get time in minutes
|
||||
duration = file.ffProbeData.streams[0].duration * 0.0166667;
|
||||
|
||||
// Set up required variables.
|
||||
let videoIdx = 0;
|
||||
let extraArguments = '';
|
||||
let bitrateSettings = '';
|
||||
// Work out currentBitrate using "Bitrate = file size / (number of minutes * .0075)"
|
||||
// Used from here https://blog.frame.io/2017/03/06/calculate-video-bitrates/
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const currentBitrate = ~~(file.file_size / (duration * 0.0075));
|
||||
// For h.264, the target bitrate matches the current bitrate, since we're not reducing quality, just changing codec
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const targetBitrate = ~~(file.file_size / (duration * 0.0075));
|
||||
// Allow some leeway under and over the targetBitrate.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const minimumBitrate = ~~(targetBitrate * 0.7);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const maximumBitrate = ~~(targetBitrate * 1.3);
|
||||
|
||||
// This shouldn't be 0, for any reason, and if it is, you should get outta there.
|
||||
if (targetBitrate === 0) {
|
||||
response.processFile = false;
|
||||
response.infoLog += 'Target bitrate could not be calculated. Skipping this plugin. \n';
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check if force_conform option is checked.
|
||||
// If so then check streams and add any extra parameters required to make file conform with output format.
|
||||
if (inputs.force_conform === 'true') {
|
||||
if (inputs.container.toLowerCase() === 'mkv') {
|
||||
extraArguments += '-map -0:d ';
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
if (
|
||||
file.ffProbeData.streams[i].codec_name
|
||||
.toLowerCase() === 'mov_text'
|
||||
|| file.ffProbeData.streams[i].codec_name
|
||||
.toLowerCase() === 'eia_608'
|
||||
|| file.ffProbeData.streams[i].codec_name
|
||||
.toLowerCase() === 'timed_id3'
|
||||
) {
|
||||
extraArguments += `-map -0:${i} `;
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inputs.container.toLowerCase() === 'mp4') {
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
try {
|
||||
if (
|
||||
file.ffProbeData.streams[i].codec_name
|
||||
.toLowerCase() === 'hdmv_pgs_subtitle'
|
||||
|| file.ffProbeData.streams[i].codec_name
|
||||
.toLowerCase() === 'eia_608'
|
||||
|| file.ffProbeData.streams[i].codec_name
|
||||
.toLowerCase() === 'subrip'
|
||||
|| file.ffProbeData.streams[i].codec_name
|
||||
.toLowerCase() === 'timed_id3'
|
||||
) {
|
||||
extraArguments += `-map -0:${i} `;
|
||||
}
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go through each stream in the file
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i++) {
|
||||
// Check if stream is video
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'video') {
|
||||
// Check if the video stream is mjpeg/png, and removes it.
|
||||
// These are embedded image streams which ffmpeg doesn't like to work with as a video stream
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'mjpeg'
|
||||
|| file.ffProbeData.streams[i].codec_name.toLowerCase() === 'png') {
|
||||
response.infoLog += 'File Contains mjpeg / png video streams, removing.';
|
||||
extraArguments += `-map -v:${videoIdx} `;
|
||||
}
|
||||
|
||||
// If video is h264, and container matches desired container, we don't need to do anything
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'h264' && file.container === inputs.container) {
|
||||
response.processFile = false;
|
||||
response.infoLog += `File is already H264 and in ${inputs.container} \n`;
|
||||
return response;
|
||||
}
|
||||
|
||||
// if video is h264, but container does NOT match desired container, do a remux
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'h264' && file.container !== inputs.container) {
|
||||
response.processFile = true;
|
||||
response.infoLog += `File is already H264 but file is not in ${inputs.container}. Remuxing \n`;
|
||||
response.preset = `, -map 0 -c copy ${extraArguments}`;
|
||||
return response;
|
||||
}
|
||||
|
||||
// Increment videoIdx.
|
||||
videoIdx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Set bitrateSettings variable using bitrate information calulcated earlier.
|
||||
bitrateSettings = `-b:v ${targetBitrate}k -minrate ${minimumBitrate}k `
|
||||
+ `-maxrate ${maximumBitrate}k -bufsize ${currentBitrate}k`;
|
||||
// Print to infoLog information around file & bitrate settings.
|
||||
response.infoLog += `Container for output selected as ${inputs.container}. \n`;
|
||||
response.infoLog += `Current bitrate = ${currentBitrate} \n`;
|
||||
response.infoLog += 'Bitrate settings: \n';
|
||||
response.infoLog += `Target = ${targetBitrate} \n`;
|
||||
response.infoLog += `Minimum = ${minimumBitrate} \n`;
|
||||
response.infoLog += `Maximum = ${maximumBitrate} \n`;
|
||||
|
||||
// Codec will be checked so it can be transcoded correctly
|
||||
if (file.video_codec_name === 'h263') {
|
||||
response.preset = '-c:v h263_cuvid';
|
||||
} else if (file.video_codec_name === 'hevc') {
|
||||
response.preset = '';
|
||||
} else if (file.video_codec_name === 'av1') {
|
||||
response.preset = '';
|
||||
} else if (file.video_codec_name === 'vp9') {
|
||||
response.preset = '';
|
||||
} else if (file.video_codec_name === 'mjpeg') {
|
||||
response.preset = '-c:v mjpeg_cuvid';
|
||||
} else if (file.video_codec_name === 'mpeg1') {
|
||||
response.preset = '-c:v mpeg1_cuvid';
|
||||
} else if (file.video_codec_name === 'mpeg2') {
|
||||
response.preset = '-c:v mpeg2_cuvid';
|
||||
} else if (file.video_codec_name === 'vc1') {
|
||||
response.preset = '-c:v vc1_cuvid';
|
||||
} else if (file.video_codec_name === 'vp8') {
|
||||
response.preset = '-c:v vp8_cuvid';
|
||||
}
|
||||
|
||||
response.preset += `,-map 0 -c:v h264_nvenc -preset fast -crf 23 -tune film ${bitrateSettings} `
|
||||
+ `-c:a copy -c:s copy -max_muxing_queue_size 9999 -pix_fmt yuv420p ${extraArguments}`;
|
||||
response.processFile = true;
|
||||
response.infoLog += 'File is not h264. Transcoding. \n';
|
||||
return response;
|
||||
};
|
||||
@ -0,0 +1,478 @@
|
||||
/* eslint max-classes-per-file: ["error", 2] */
|
||||
function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_VP92_VP9_Match_Bitrate_One_Pass',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'VP9 Encoding Match Bitrate 1 Pass System',
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: `Will run through linvpx-vp9 and follow the contrained quality contraints. Will also encode audio to
|
||||
opus using libopus. Allows user-input on the desired constrained quality amount for each video resolution with
|
||||
defaults if none are given.`,
|
||||
Version: '1.00',
|
||||
Link: 'https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/Tdarr_Plugin_075a_FFMPEG_HEVC_Generic.js',
|
||||
Tags: 'pre-processing,ffmpeg,vp9',
|
||||
Inputs: [
|
||||
{
|
||||
name: 'CQ_240p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 32',
|
||||
},
|
||||
{
|
||||
name: 'CG_360p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 31',
|
||||
},
|
||||
{
|
||||
name: 'CQ_480p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 28',
|
||||
},
|
||||
{
|
||||
name: 'CQ_720p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 27',
|
||||
},
|
||||
{
|
||||
name: 'CQ_1080p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 26',
|
||||
},
|
||||
{
|
||||
name: 'CQ_4KUHD',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 15',
|
||||
},
|
||||
{
|
||||
name: 'CQ_8KUHD',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 15',
|
||||
},
|
||||
{
|
||||
name: 'audio_language',
|
||||
tooltip: `
|
||||
Specify language tag/s here for the audio tracks you'd like to keep, recommended to keep "und" as this\\n
|
||||
stands for undertermined, some files may not have the language specified. Must follow ISO-639-2 3 letter\\n
|
||||
format. https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
|
||||
\\nExample:\\n
|
||||
eng
|
||||
\\nExample:\\n
|
||||
eng,und
|
||||
\\nExample:\\n
|
||||
eng,und,jap`,
|
||||
},
|
||||
{
|
||||
name: 'audio_commentary',
|
||||
tooltip: `Specify if audio tracks that contain commentary/description should be removed.
|
||||
\\nExample:\\n
|
||||
true
|
||||
\\nExample:\\n
|
||||
false`,
|
||||
},
|
||||
{
|
||||
name: 'subtitle_language',
|
||||
tooltip: `Specify language tag/s here for the subtitle tracks you'd like to keep. Must follow ISO-639-2 3 \\n
|
||||
letter format. https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
|
||||
\\nExample:\\n
|
||||
eng
|
||||
\\nExample:\\n
|
||||
eng,jap`,
|
||||
},
|
||||
{
|
||||
name: 'subtitle_commentary',
|
||||
tooltip: `Specify if subtitle tracks that contain commentary/description should be removed.
|
||||
\\nExample:\\n
|
||||
true
|
||||
\\nExample:\\n
|
||||
false`,
|
||||
},
|
||||
{
|
||||
name: 'remove_mjpeg',
|
||||
tooltip: `Specify if mjpeg codecs should be removed.
|
||||
\\nExample:\\n
|
||||
true
|
||||
\\nExample:\\n
|
||||
false`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// #region Helper Classes/Modules
|
||||
|
||||
/**
|
||||
* Handles logging in a standardised way.
|
||||
*/
|
||||
class Log {
|
||||
constructor() {
|
||||
this.entries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} entry the log entry string
|
||||
*/
|
||||
Add(entry) {
|
||||
this.entries.push(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} entry the log entry string
|
||||
*/
|
||||
AddSuccess(entry) {
|
||||
this.entries.push(`☑ ${entry}`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} entry the log entry string
|
||||
*/
|
||||
AddError(entry) {
|
||||
this.entries.push(`☒ ${entry}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log lines separated by new line delimiter.
|
||||
*/
|
||||
GetLogData() {
|
||||
return this.entries.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the storage of FFmpeg configuration.
|
||||
*/
|
||||
class Configurator {
|
||||
constructor(defaultOutputSettings = null) {
|
||||
this.shouldProcess = false;
|
||||
this.outputSettings = defaultOutputSettings || [];
|
||||
this.inputSettings = [];
|
||||
}
|
||||
|
||||
AddInputSetting(configuration) {
|
||||
this.inputSettings.push(configuration);
|
||||
}
|
||||
|
||||
AddOutputSetting(configuration) {
|
||||
this.shouldProcess = true;
|
||||
this.outputSettings.push(configuration);
|
||||
}
|
||||
|
||||
ResetOutputSetting(configuration) {
|
||||
this.shouldProcess = false;
|
||||
this.outputSettings = configuration;
|
||||
}
|
||||
|
||||
RemoveOutputSetting(configuration) {
|
||||
const index = this.outputSettings.indexOf(configuration);
|
||||
|
||||
if (index === -1) return;
|
||||
this.outputSettings.splice(index, 1);
|
||||
}
|
||||
|
||||
RemoveAllConfigurationsBySearchString(search_string) {
|
||||
for (let i = this.outputSettings.length - 1; i >= 0; i -= 1) {
|
||||
if (this.outputSettings[i].includes(search_string)) {
|
||||
this.outputSettings.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetOutputSettings() {
|
||||
return this.outputSettings.join(' ');
|
||||
}
|
||||
|
||||
GetInputSettings() {
|
||||
return this.inputSettings.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops over the file streams and executes the given method on
|
||||
* each stream when the matching codec_type is found.
|
||||
* @param {Object} file the file.
|
||||
* @param {string} type the typeo of stream.
|
||||
* @param {function} method the method to call.
|
||||
*/
|
||||
function loopOverStreamsOfType(file, type, method) {
|
||||
let id = 0;
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === type) {
|
||||
method(file.ffProbeData.streams[i], id);
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildAudioConfiguration(inputs, file, logger) {
|
||||
const configuration = new Configurator(['-c:a copy']);
|
||||
let stream_count = 0;
|
||||
let streams_removing = 0;
|
||||
const languages = inputs.audio_language.split(',');
|
||||
let opusFormat = false;
|
||||
let mappingFamily = false;
|
||||
|
||||
loopOverStreamsOfType(file, 'audio', (stream, id) => {
|
||||
stream_count += 1;
|
||||
|
||||
if (stream.codec_name !== 'opus' && !opusFormat) {
|
||||
logger.AddError('Audio is not in proper codec, will format');
|
||||
configuration.RemoveOutputSetting('-c:a copy');
|
||||
configuration.AddOutputSetting('-c:a libopus');
|
||||
opusFormat = true;
|
||||
}
|
||||
|
||||
if (
|
||||
(stream.channel_layout === '5.1(side)' || (stream.codec_name === 'eac3' && stream.channels === 6)) && opusFormat
|
||||
) {
|
||||
logger.AddSuccess(
|
||||
`Determined audio to be ${stream.channel_layout}, adding mapping configuration for proper conversion`,
|
||||
);
|
||||
configuration.AddOutputSetting(
|
||||
`-filter_complex "[0:a:${id}]channelmap=channel_layout=5.1"`,
|
||||
);
|
||||
if (!mappingFamily) {
|
||||
configuration.AddOutputSetting('-mapping_family 1');
|
||||
mappingFamily = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.channel_layout === '6.1(back)' && opusFormat) {
|
||||
logger.AddSuccess(
|
||||
`Determined audio to be ${stream.channel_layout}, adding mapping configuration for proper conversion`,
|
||||
);
|
||||
configuration.AddOutputSetting(
|
||||
`-filter_complex "[0:a:${id}]channelmap=channel_layout=6.1"`,
|
||||
);
|
||||
if (!mappingFamily) {
|
||||
configuration.AddOutputSetting('-mapping_family 1');
|
||||
mappingFamily = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
'tags' in stream && 'title' in stream.tags && inputs.audio_commentary.toLowerCase() === 'true'
|
||||
) {
|
||||
if (
|
||||
stream.tags.title.toLowerCase().includes('commentary')
|
||||
|| stream.tags.title.toLowerCase().includes('description')
|
||||
|| stream.tags.title.toLowerCase().includes('sdh')
|
||||
) {
|
||||
streams_removing += 1;
|
||||
configuration.AddOutputSetting(`-map -0:a:${id}`);
|
||||
logger.AddError(
|
||||
`Removing Commentary or Description audio track: ${stream.tags.title}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ('tags' in stream) {
|
||||
// Remove unwanted languages
|
||||
if ('language' in stream.tags) {
|
||||
if (languages.indexOf(stream.tags.language.toLowerCase()) === -1) {
|
||||
configuration.AddOutputSetting(`-map -0:a:${id}`);
|
||||
streams_removing += 1;
|
||||
logger.AddError(
|
||||
`Removing audio track in language ${stream.tags.language}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (stream_count === streams_removing) {
|
||||
logger.AddError(
|
||||
'*** All audio tracks would have been removed, removing all delete entries',
|
||||
);
|
||||
configuration.RemoveAllConfigurationsBySearchString('-map -0');
|
||||
}
|
||||
|
||||
if (!configuration.shouldProcess) {
|
||||
logger.AddSuccess('No audio processing necessary');
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
function buildVideoConfiguration(inputs, file, logger) {
|
||||
const configuration = new Configurator(['-map 0', '-map -0:d', '-c:v copy']);
|
||||
|
||||
loopOverStreamsOfType(file, 'video', (stream, id) => {
|
||||
if (stream.codec_name === 'mjpeg') {
|
||||
if (inputs.remove_mjpeg.toLowerCase() === 'true') {
|
||||
logger.AddError('Removing mjpeg');
|
||||
configuration.AddOutputSetting(`-map -0:v:${id}`);
|
||||
} else {
|
||||
configuration.AddOutputSetting(`-map -v:${id}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.codec_name === 'vp9' && file.container === 'webm') {
|
||||
logger.AddSuccess('File is in proper video format');
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.codec_name === 'vp9' && file.container !== 'webm') {
|
||||
configuration.AddOutputSetting('-c:v copy');
|
||||
logger.AddError(
|
||||
'File is in proper codec but not write container. Will remux',
|
||||
);
|
||||
}
|
||||
|
||||
let speed = 1;
|
||||
let targetQuality = 32;
|
||||
let tileColumns = 0;
|
||||
const threadCount = 64;
|
||||
if (file.video_resolution === '240p') {
|
||||
targetQuality = inputs.CQ_240p || 32;
|
||||
tileColumns = 0;
|
||||
speed = 1;
|
||||
} else if (file.video_resolution === '360p' || file.video_resolution === '576p') {
|
||||
targetQuality = inputs.CQ_360p || 31;
|
||||
tileColumns = 1;
|
||||
speed = 1;
|
||||
} else if (file.video_resolution === '480p') {
|
||||
targetQuality = inputs.CQ_480p || 28;
|
||||
tileColumns = 1;
|
||||
speed = 1;
|
||||
} else if (file.video_resolution === '720p') {
|
||||
targetQuality = inputs.CQ_720p || 27;
|
||||
tileColumns = 2;
|
||||
speed = 2;
|
||||
} else if (file.video_resolution === '1080p') {
|
||||
targetQuality = inputs.CQ_1080p || 26;
|
||||
tileColumns = 2;
|
||||
speed = 2;
|
||||
} else if (
|
||||
file.video_resolution === '1440p' || file.video_resolution === '2560p' || file.video_resolution === '4KUHD'
|
||||
) {
|
||||
targetQuality = inputs.CQ_4KUHD || 15;
|
||||
tileColumns = 3;
|
||||
speed = 2;
|
||||
} else if (file.video_resolution === '8KUHD') {
|
||||
targetQuality = inputs.CQ_8KUHD || 15;
|
||||
tileColumns = 3;
|
||||
speed = 2;
|
||||
}
|
||||
|
||||
configuration.RemoveOutputSetting('-c:v copy');
|
||||
configuration.AddOutputSetting(
|
||||
`-pix_fmt yuv420p10le -c:v libvpx-vp9 -b:v 0 -crf ${targetQuality} -threads ${threadCount} -speed ${speed}
|
||||
-quality good -static-thresh 0 -tile-columns ${tileColumns} -tile-rows 0 -frame-parallel 0 -row-mt 1
|
||||
-aq-mode 0 -g 240`,
|
||||
);
|
||||
|
||||
logger.AddError('Transcoding file to VP9');
|
||||
});
|
||||
|
||||
if (!configuration.shouldProcess) {
|
||||
logger.AddSuccess('No video processing necessary');
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
function buildSubtitleConfiguration(inputs, file, logger) {
|
||||
const configuration = new Configurator(['-c:s copy']);
|
||||
// webvtt
|
||||
|
||||
const languages = inputs.subtitle_language.split(',');
|
||||
let webvttFormat = false;
|
||||
// if (languages.length === 0) return configuration;
|
||||
|
||||
loopOverStreamsOfType(file, 'subtitle', (stream, id) => {
|
||||
if (
|
||||
stream.codec_name === 'hdmv_pgs_subtitle'
|
||||
|| stream.codec_name === 'eia_608'
|
||||
|| stream.codec_name === 'dvd_subtitle'
|
||||
) {
|
||||
logger.AddError(
|
||||
`Removing subtitle in invalid codec ${stream.codec_name}`,
|
||||
);
|
||||
configuration.AddOutputSetting(`-map -0:s:${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('tags' in stream) {
|
||||
// Remove unwated languages
|
||||
if ('language' in stream.tags) {
|
||||
if (languages.indexOf(stream.tags.language.toLowerCase()) === -1) {
|
||||
configuration.AddOutputSetting(`-map -0:s:${id}`);
|
||||
logger.AddError(
|
||||
`Removing subtitle in language ${stream.tags.language}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove commentary subtitles
|
||||
if (
|
||||
'title' in stream.tags
|
||||
&& inputs.subtitle_commentary.toLowerCase() === 'true'
|
||||
) {
|
||||
if (
|
||||
stream.tags.title.toLowerCase().includes('commentary')
|
||||
|| stream.tags.title.toLowerCase().includes('description')
|
||||
|| stream.tags.title.toLowerCase().includes('sdh')
|
||||
) {
|
||||
configuration.AddOutputSetting(`-map -0:s:${id}`);
|
||||
logger.AddError(
|
||||
`Removing Commentary or Description subtitle: ${stream.tags.title}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.codec_name !== 'webvtt' && !webvttFormat) {
|
||||
logger.AddError('Formatting subtitles to webvtt format');
|
||||
configuration.RemoveOutputSetting('-c:s copy');
|
||||
configuration.AddOutputSetting('-c:s webvtt');
|
||||
webvttFormat = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!configuration.shouldProcess) {
|
||||
logger.AddSuccess('No subtitle processing necessary');
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
function plugin(file, librarySettings, inputs) {
|
||||
// Must return this object
|
||||
const response = {
|
||||
container: '.webm',
|
||||
FFmpegMode: true,
|
||||
handBrakeMode: false,
|
||||
infoLog: '',
|
||||
processFile: false,
|
||||
preset: '',
|
||||
reQueueAfter: true,
|
||||
};
|
||||
|
||||
const logger = new Log();
|
||||
|
||||
const audioSettings = buildAudioConfiguration(inputs, file, logger);
|
||||
const videoSettings = buildVideoConfiguration(inputs, file, logger);
|
||||
const subtitleSettings = buildSubtitleConfiguration(inputs, file, logger);
|
||||
|
||||
response.processFile = audioSettings.shouldProcess
|
||||
|| videoSettings.shouldProcess
|
||||
|| subtitleSettings.shouldProcess;
|
||||
|
||||
if (!response.processFile) {
|
||||
logger.AddSuccess('No need to process file');
|
||||
}
|
||||
|
||||
response.preset = `${videoSettings.GetInputSettings()},${videoSettings.GetOutputSettings()}
|
||||
${audioSettings.GetOutputSettings()} ${subtitleSettings.GetOutputSettings()}`;
|
||||
response.infoLog += logger.GetLogData();
|
||||
return response;
|
||||
}
|
||||
|
||||
module.exports.details = details;
|
||||
module.exports.plugin = plugin;
|
||||
@ -0,0 +1,272 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
module.exports.dependencies = ['axios', '@cospired/i18n-iso-languages', 'path'];
|
||||
const details = () => ({
|
||||
id: 'Tdarr_Plugin_henk_Keep_Native_Lang_Plus_Eng',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'Remove all langs except native and English',
|
||||
Type: 'Audio',
|
||||
Operation: 'Transcode',
|
||||
Description: `This plugin will remove all language audio tracks except the 'native'
|
||||
(requires TMDB api key) and English.
|
||||
'Native' languages are the ones that are listed on imdb. It does an API call to
|
||||
Radarr, Sonarr to check if the movie/series exists and grabs the IMDB id. As a last resort it
|
||||
falls back to the IMDB id in the filename.`,
|
||||
Version: '1.00',
|
||||
Link: 'https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/'
|
||||
+ 'Tdarr_Plugin_henk_Keep_Native_Lang_Plus_Eng.js',
|
||||
Tags: 'pre-processing,configurable',
|
||||
|
||||
Inputs: [
|
||||
{
|
||||
name: 'user_langs',
|
||||
tooltip: 'Input a comma separated list of ISO-639-2 languages. It will still keep English and undefined tracks.'
|
||||
+ '(https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes 639-2 column)'
|
||||
+ '\\nExample:\\n'
|
||||
+ 'nld,nor',
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
tooltip: 'Priority for either Radarr or Sonarr. Leaving it empty defaults to Radarr first.'
|
||||
+ '\\nExample:\\n'
|
||||
+ 'sonarr',
|
||||
},
|
||||
{
|
||||
name: 'api_key',
|
||||
tooltip: 'Input your TMDB api (v3) key here. (https://www.themoviedb.org/)',
|
||||
},
|
||||
{
|
||||
name: 'radarr_api_key',
|
||||
tooltip: 'Input your Radarr api key here.',
|
||||
},
|
||||
{
|
||||
name: 'radarr_url',
|
||||
tooltip: 'Input your Radarr url here. (Without http://). Do include the port.'
|
||||
+ '\\nExample:\\n'
|
||||
+ '192.168.1.2:7878',
|
||||
},
|
||||
{
|
||||
name: 'sonarr_api_key',
|
||||
tooltip: 'Input your Sonarr api key here.',
|
||||
},
|
||||
{
|
||||
name: 'sonarr_url',
|
||||
tooltip: 'Input your Sonarr url here. (Without http://). Do include the port.'
|
||||
+ '\\nExample:\\n'
|
||||
+ '192.168.1.2:8989',
|
||||
},
|
||||
],
|
||||
});
|
||||
const response = {
|
||||
processFile: false,
|
||||
preset: ', -map 0 ',
|
||||
container: '.',
|
||||
handBrakeMode: false,
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: false,
|
||||
infoLog: '',
|
||||
};
|
||||
|
||||
const processStreams = (result, file, user_langs) => {
|
||||
// eslint-disable-next-line global-require,import/no-unresolved
|
||||
const languages = require('@cospired/i18n-iso-languages');
|
||||
const tracks = {
|
||||
keep: [],
|
||||
remove: [],
|
||||
remLangs: '',
|
||||
};
|
||||
let streamIndex = 0;
|
||||
|
||||
const langsTemp = result.original_language;
|
||||
let langs = [];
|
||||
|
||||
if (Array.isArray(langsTemp)) {
|
||||
// For loop because I thought some imdb stuff returns multiple languages
|
||||
// Translates 'en' to 'eng', because imdb uses a different format compared to ffmpeg
|
||||
for (let i = 0; i < langsTemp.length; i += 1) {
|
||||
langs.push(languages.alpha2ToAlpha3B(langsTemp));
|
||||
}
|
||||
} else {
|
||||
langs.push(languages.alpha2ToAlpha3B(langsTemp));
|
||||
}
|
||||
|
||||
if (user_langs) {
|
||||
langs = langs.concat(user_langs);
|
||||
}
|
||||
if (!langs.includes('eng')) langs.push('eng');
|
||||
if (!langs.includes('und')) langs.push('und');
|
||||
|
||||
response.infoLog += 'Keeping languages: ';
|
||||
// Print languages to UI
|
||||
langs.forEach((l) => {
|
||||
response.infoLog += `${languages.getName(l, 'en')}, `;
|
||||
});
|
||||
|
||||
response.infoLog = `${response.infoLog.slice(0, -2)}\n`;
|
||||
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
|
||||
const stream = file.ffProbeData.streams[i];
|
||||
|
||||
if (stream.codec_type === 'audio') {
|
||||
if (!stream.tags) {
|
||||
response.infoLog += `☒No tags found on audio track ${streamIndex}. Keeping it. \n`;
|
||||
tracks.keep.push(streamIndex);
|
||||
streamIndex += 1;
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
if (stream.tags.language) {
|
||||
if (langs.includes(stream.tags.language)) {
|
||||
tracks.keep.push(streamIndex);
|
||||
} else {
|
||||
tracks.remove.push(streamIndex);
|
||||
response.preset += `-map -0:a:${streamIndex} `;
|
||||
tracks.remLangs += `${languages.getName(stream.tags.language, 'en')}, `;
|
||||
}
|
||||
streamIndex += 1;
|
||||
} else {
|
||||
response.infoLog += `☒No language tag found on audio track ${streamIndex}. Keeping it. \n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
response.preset += ' -c copy -max_muxing_queue_size 9999';
|
||||
return tracks;
|
||||
};
|
||||
|
||||
const tmdbApi = async (filename, api_key, axios) => {
|
||||
let fileName;
|
||||
// If filename begins with tt, it's already an imdb id
|
||||
if (filename) {
|
||||
if (filename.substr(0, 2) === 'tt') {
|
||||
fileName = filename;
|
||||
} else {
|
||||
const idRegex = /(tt\d{7,8})/;
|
||||
const fileMatch = filename.match(idRegex);
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
if (fileMatch) fileName = fileMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (fileName) {
|
||||
const result = await axios.get(`https://api.themoviedb.org/3/find/${fileName}?api_key=`
|
||||
+ `${api_key}&language=en-US&external_source=imdb_id`)
|
||||
.then((resp) => (resp.data.movie_results.length > 0 ? resp.data.movie_results[0] : resp.data.tv_results[0]));
|
||||
|
||||
if (!result) {
|
||||
response.infoLog += '☒No IMDB result was found. \n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
const parseArrResponse = async (body, filePath, arr) => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (arr) {
|
||||
case 'radarr':
|
||||
// filePath = file
|
||||
for (let i = 0; i < body.length; i += 1) {
|
||||
if (body[i].movieFile) {
|
||||
if (body[i].movieFile.relativePath) {
|
||||
if (body[i].movieFile.relativePath === filePath) {
|
||||
return body[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'sonarr':
|
||||
// filePath = directory the file is in
|
||||
// eslint-disable-next-line global-require,import/no-unresolved
|
||||
const path = require('path');
|
||||
for (let i = 0; i < body.length; i += 1) {
|
||||
if (body[i].path) {
|
||||
if (path.basename(body[i].path) === path.basename(path.dirname(filePath))) {
|
||||
return body[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const plugin = async (file, librarySettings, inputs) => {
|
||||
// eslint-disable-next-line global-require,import/no-unresolved
|
||||
const axios = require('axios').default;
|
||||
response.container = `.${file.container}`;
|
||||
let prio = ['radarr', 'sonarr'];
|
||||
let radarrResult = null;
|
||||
let sonarrResult = null;
|
||||
let tmdbResult = null;
|
||||
|
||||
if (inputs.priority) {
|
||||
if (inputs.priority === 'sonarr') {
|
||||
prio = ['sonarr', 'radarr'];
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < prio.length; i += 1) {
|
||||
let imdbId;
|
||||
// eslint-disable-next-line default-case
|
||||
switch (prio[i]) {
|
||||
case 'radarr':
|
||||
if (tmdbResult) break;
|
||||
if (inputs.radarr_api_key) {
|
||||
radarrResult = await parseArrResponse(
|
||||
await axios.get(`http://${inputs.radarr_url}/api/v3/movie?apiKey=${inputs.radarr_api_key}`)
|
||||
.then((resp) => resp.data),
|
||||
file.meta.FileName, 'radarr',
|
||||
);
|
||||
|
||||
if (radarrResult) {
|
||||
imdbId = radarrResult.imdbId;
|
||||
response.infoLog += `Grabbed ID (${imdbId}) from Radarr \n`;
|
||||
} else {
|
||||
response.infoLog += 'Couldn\'t grab ID from Radarr/Sonarr, grabbing it from file name \n';
|
||||
imdbId = file.meta.FileName;
|
||||
}
|
||||
tmdbResult = await tmdbApi(imdbId, inputs.api_key, axios);
|
||||
}
|
||||
break;
|
||||
case 'sonarr':
|
||||
if (tmdbResult) break;
|
||||
if (inputs.sonarr_api_key) {
|
||||
sonarrResult = await parseArrResponse(
|
||||
await axios.get(`http://${inputs.sonarr_url}/api/series?apikey=${inputs.sonarr_api_key}`)
|
||||
.then((resp) => resp.data),
|
||||
file.meta.Directory, 'sonarr',
|
||||
);
|
||||
|
||||
if (sonarrResult) {
|
||||
imdbId = sonarrResult.imdbId;
|
||||
response.infoLog += `Grabbed ID (${imdbId}) from Sonarr \n`;
|
||||
} else {
|
||||
response.infoLog += 'Couldn\'t grab ID from Radarr/Sonarr, grabbing it from file name \n';
|
||||
imdbId = file.meta.FileName;
|
||||
}
|
||||
tmdbResult = await tmdbApi(imdbId, inputs.api_key, axios);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tmdbResult) {
|
||||
const tracks = processStreams(tmdbResult, file, inputs.user_langs ? inputs.user_langs.split(',') : '');
|
||||
|
||||
if (tracks.remove.length > 0) {
|
||||
if (tracks.keep.length > 0) {
|
||||
response.infoLog += `☑Removing tracks with languages: ${tracks.remLangs.slice(0, -2)}. \n`;
|
||||
response.processFile = true;
|
||||
response.infoLog += '\n';
|
||||
} else {
|
||||
response.infoLog += '☒Cancelling plugin otherwise all audio tracks would be removed. \n';
|
||||
}
|
||||
} else {
|
||||
response.infoLog += '☒No audio tracks to be removed. \n';
|
||||
}
|
||||
} else {
|
||||
response.infoLog += '☒Couldn\'t find the IMDB id of this file. Skipping. \n';
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
module.exports.details = details;
|
||||
module.exports.plugin = plugin;
|
||||
@ -0,0 +1,190 @@
|
||||
function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_vdka_Tiered_CPU_CRF_Based_Configurable',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'Tiered FFMPEG CPU CRF Based Configurable',
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: `[Contains built-in filter] This plugin uses different CRF values depending on resolution,
|
||||
the CRF value is configurable per resolution.
|
||||
FFmpeg Preset can be configured, uses slow by default.
|
||||
If files are not in hevc they will be transcoded.
|
||||
The output container is mkv. \n\n`,
|
||||
Version: '1.00',
|
||||
Link:
|
||||
'https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/'
|
||||
+ ' Tdarr_Plugin_vdka_Tiered_CPU_CRF_Based_Configurable.js',
|
||||
Tags: 'pre-processing,ffmpeg,video only,h265,configurable',
|
||||
|
||||
Inputs: [
|
||||
{
|
||||
name: 'sdCRF',
|
||||
tooltip: `Enter the CRF value you want for 480p and 576p content.
|
||||
\n Defaults to 20 (0-51, lower = higher quality, bigger file)
|
||||
\\nExample:\\n
|
||||
|
||||
19`,
|
||||
},
|
||||
{
|
||||
name: 'hdCRF',
|
||||
tooltip: `Enter the CRF value you want for 720p content.
|
||||
\n Defaults to 22 (0-51, lower = higher quality, bigger file)
|
||||
|
||||
\\nExample:\\n
|
||||
21`,
|
||||
},
|
||||
{
|
||||
name: 'fullhdCRF',
|
||||
tooltip: `Enter the CRF value you want for 1080p content.
|
||||
\n Defaults to 24 (0-51, lower = higher quality, bigger file)
|
||||
|
||||
\\nExample:\\n
|
||||
23`,
|
||||
},
|
||||
{
|
||||
name: 'uhdCRF',
|
||||
tooltip: `Enter the CRF value you want for 4K/UHD/2160p content.
|
||||
\n Defaults to 28 (0-51, lower = higher quality, bigger file)
|
||||
|
||||
\\nExample:\\n
|
||||
26`,
|
||||
},
|
||||
{
|
||||
name: 'bframe',
|
||||
tooltip: `Specify amount of b-frames to use, 0-16, defaults to 8.
|
||||
|
||||
\\nExample:\\n
|
||||
8`,
|
||||
},
|
||||
{
|
||||
name: 'ffmpegPreset',
|
||||
tooltip: `Enter the ffmpeg preset you want, leave blank for default (slow)
|
||||
|
||||
\\nExample:\\n
|
||||
slow
|
||||
|
||||
\\nExample:\\n
|
||||
medium
|
||||
|
||||
\\nExample:\\n
|
||||
fast
|
||||
|
||||
\\nExample:\\n
|
||||
veryfast`,
|
||||
},
|
||||
{
|
||||
name: 'sdDisabled',
|
||||
tooltip: `Input "true" if you want to skip SD (480p and 576p) files
|
||||
|
||||
\\nExample:\\n
|
||||
true`,
|
||||
},
|
||||
{
|
||||
name: 'uhdDisabled',
|
||||
tooltip: `Input "true" if you want to skip 4k (UHD) files
|
||||
|
||||
\\nExample:\\n
|
||||
true`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function plugin(file, librarySettings, inputs) {
|
||||
let crf;
|
||||
// default values that will be returned
|
||||
const response = {
|
||||
processFile: true,
|
||||
preset: '',
|
||||
container: '.mkv',
|
||||
handBrakeMode: false,
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: true,
|
||||
infoLog: '',
|
||||
};
|
||||
|
||||
// check if the file is a video, if not the function will be stopped immediately
|
||||
if (file.fileMedium !== 'video') {
|
||||
response.processFile = false;
|
||||
response.infoLog += '☒File is not a video! \n';
|
||||
return response;
|
||||
}
|
||||
response.infoLog += '☑File is a video! \n';
|
||||
|
||||
// check if the file is SD and sdDisable is enabled
|
||||
// skip this plugin if so
|
||||
if (['480p', '576p'].includes(file.video_resolution) && inputs.sdDisabled) {
|
||||
response.processFile = false;
|
||||
response.infoLog += '☒File is SD, not processing\n';
|
||||
return response;
|
||||
}
|
||||
|
||||
// check if the file is 4k and 4kDisable is enabled
|
||||
// skip this plugin if so
|
||||
if (file.video_resolution === '4KUHD' && inputs.uhdDisabled) {
|
||||
response.processFile = false;
|
||||
response.infoLog += '☒File is 4k/UHD, not processing\n';
|
||||
return response;
|
||||
}
|
||||
|
||||
// check if the file is already hevc
|
||||
// it will not be transcoded if true and the plugin will be stopped immediately
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
|
||||
if (file.ffProbeData.streams[i].codec_name.toLowerCase() === 'hevc') {
|
||||
response.processFile = false;
|
||||
response.infoLog += '☑File is already in hevc! \n';
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// if we made it to this point it is safe to assume there is no hevc stream
|
||||
response.infoLog += '☒File is not hevc!\n';
|
||||
|
||||
// set sane input defaults if not configured
|
||||
const sdCRF = inputs.sdCRF ? inputs.sdCRF : 20;
|
||||
const hdCRF = inputs.hdCRF ? inputs.hdCRF : 22;
|
||||
const fullhdCRF = inputs.fullhdCRF ? inputs.fullhdCRF : 24;
|
||||
const uhdCRF = inputs.uhdCRF ? inputs.uhdCRF : 28;
|
||||
const bframe = inputs.bframe ? inputs.bframe : 8;
|
||||
|
||||
// set preset to slow if not configured
|
||||
let ffmpegPreset = 'slow';
|
||||
if (!inputs.ffmpegPreset) {
|
||||
response.infoLog += '☑Preset not set, defaulting to slow\n';
|
||||
} else {
|
||||
ffmpegPreset = `${inputs.ffmpegPreset}`;
|
||||
response.infoLog += `☑Preset set as ${inputs.ffmpegPreset}\n`;
|
||||
}
|
||||
|
||||
// set crf by resolution
|
||||
switch (file.video_resolution) {
|
||||
case '480p':
|
||||
case '576p':
|
||||
crf = sdCRF;
|
||||
break;
|
||||
case '720p':
|
||||
crf = hdCRF;
|
||||
break;
|
||||
case '1080p':
|
||||
crf = fullhdCRF;
|
||||
break;
|
||||
case '4KUHD':
|
||||
crf = uhdCRF;
|
||||
break;
|
||||
default:
|
||||
response.infoLog += 'Could for some reason not detect resolution, plugin will not proceed. \n';
|
||||
response.processFile = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
// encoding settings
|
||||
response.preset += `,-map 0 -dn -c:v libx265 -preset ${ffmpegPreset}`
|
||||
+ ` -x265-params crf=${crf}:bframes=${bframe}:rc-lookahead=32:ref=6:b-intra=1:aq-mode=3`
|
||||
+ ' -a53cc 0 -c:a copy -c:s copy -max_muxing_queue_size 9999';
|
||||
response.infoLog += `☑File is ${file.video_resolution}, using CRF value of ${crf}!\n`;
|
||||
response.infoLog += 'File is being transcoded!\n';
|
||||
|
||||
return response;
|
||||
}
|
||||
module.exports.details = details;
|
||||
module.exports.plugin = plugin;
|
||||
@ -1,62 +1,51 @@
|
||||
/* eslint-disable */
|
||||
function details() {
|
||||
return {
|
||||
id: "Tdarr_Plugin_x7ac_Remove_Closed_Captions",
|
||||
Stage: "Pre-processing",
|
||||
Name: "Remove closed captions",
|
||||
Type: "Video",
|
||||
Operation: "Remux",
|
||||
id: 'Tdarr_Plugin_x7ac_Remove_Closed_Captions',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'Remove burned closed captions',
|
||||
Type: 'Video',
|
||||
Operation: 'Remux',
|
||||
Description:
|
||||
"[Contains built-in filter] If detected, closed captions (XDS,608,708) will be removed.",
|
||||
Version: "1.00",
|
||||
'[Contains built-in filter] If detected, closed captions (XDS,608,708) will be removed from streams.',
|
||||
Version: '1.01',
|
||||
Link:
|
||||
"https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/Tdarr_Plugin_x7ac_Remove_Closed_Captions.js",
|
||||
Tags: "pre-processing,ffmpeg,subtitle only",
|
||||
'https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/Tdarr_Plugin_x7ac_Remove_Closed_Captions.js',
|
||||
Tags: 'pre-processing,ffmpeg,subtitle only',
|
||||
};
|
||||
}
|
||||
|
||||
function plugin(file) {
|
||||
//Must return this object
|
||||
|
||||
var response = {
|
||||
const response = {
|
||||
processFile: false,
|
||||
preset: "",
|
||||
container: ".mp4",
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
preset: ',-map 0 -codec copy -bsf:v \"filter_units=remove_types=6\"',
|
||||
container: `.${file.container}`,
|
||||
handBrakeMode: false,
|
||||
FFmpegMode: false,
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: true,
|
||||
infoLog: "",
|
||||
infoLog: '',
|
||||
};
|
||||
|
||||
if (file.fileMedium !== "video") {
|
||||
console.log("File is not video");
|
||||
|
||||
response.infoLog += "☒File is not video \n";
|
||||
response.processFile = false;
|
||||
|
||||
if (file.fileMedium !== 'video') {
|
||||
response.infoLog += '☒File is not video \n';
|
||||
return response;
|
||||
} else {
|
||||
if (file.hasClosedCaptions === true) {
|
||||
response = {
|
||||
processFile: true,
|
||||
preset: ',-map 0 -codec copy -bsf:v "filter_units=remove_types=6"',
|
||||
container: "." + file.container,
|
||||
handBrakeMode: false,
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: true,
|
||||
infoLog: "☒This file has closed captions \n",
|
||||
};
|
||||
|
||||
return response;
|
||||
} else {
|
||||
response.infoLog +=
|
||||
"☑Closed captions have not been detected on this file \n";
|
||||
response.processFile = false;
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if Closed Captions are set at file level
|
||||
if (file.hasClosedCaptions) {
|
||||
response.processFile = true;
|
||||
response.infoLog += '☒This file has closed captions \n';
|
||||
return response;
|
||||
}
|
||||
// If not, check for Closed Captions in the streams
|
||||
const { streams } = file.ffProbeData;
|
||||
streams.forEach((stream) => {
|
||||
if (stream.closed_captions) {
|
||||
response.processFile = true;
|
||||
}
|
||||
});
|
||||
|
||||
response.infoLog += response.processFile ? '☒This file has burnt closed captions \n'
|
||||
: '☑Closed captions have not been detected on this file \n';
|
||||
return response;
|
||||
}
|
||||
module.exports.details = details;
|
||||
module.exports.plugin = plugin;
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
module.exports.details = function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_a9he_New_file_size_check',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'New file size check',
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: 'Give an error if new file is larger than the original \n\n',
|
||||
Version: '1.00',
|
||||
Link: '',
|
||||
Tags: '',
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.plugin = function plugin(file, librarySettings, inputs, otherArguments) {
|
||||
// Must return this object at some point in the function else plugin will fail.
|
||||
const response = {
|
||||
processFile: false,
|
||||
preset: '',
|
||||
handBrakeMode: false,
|
||||
FFmpegMode: true,
|
||||
reQueueAfter: true,
|
||||
infoLog: '',
|
||||
};
|
||||
|
||||
const newSize = file.file_size;
|
||||
const oldSize = otherArguments.originalLibraryFile.file_size;
|
||||
if (newSize > oldSize) {
|
||||
// Item will be errored in UI
|
||||
throw new Error(`Error! New file has size ${newSize} which is larger than original file ${oldSize}`);
|
||||
} else if (newSize < oldSize) {
|
||||
response.infoLog += `New file has size ${newSize} which is smaller than original file ${oldSize}`;
|
||||
}
|
||||
// if file sizes are exactly the same then file has not been transcoded yet
|
||||
|
||||
return response;
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
module.exports.details = function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_bbbc_Filter_Example',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'Filter resolutions',
|
||||
Type: 'Video',
|
||||
Operation: 'Filter',
|
||||
Description: 'This plugin prevents processing files with specified resolutions \n\n',
|
||||
Version: '1.00',
|
||||
Link: '',
|
||||
Tags: '',
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.plugin = function plugin(file) {
|
||||
const response = {
|
||||
processFile: true,
|
||||
infoLog: '',
|
||||
};
|
||||
|
||||
const resolutionsToSkip = [
|
||||
'1080p',
|
||||
'4KUHD',
|
||||
];
|
||||
|
||||
for (let i = 0; i < resolutionsToSkip.length; i += 1) {
|
||||
if (file.video_resolution === resolutionsToSkip[i]) {
|
||||
response.processFile = false;
|
||||
response.infoLog += `Filter preventing processing. File has resolution ${resolutionsToSkip[i]}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
Loading…
Reference in new issue