diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml index 2747f2617..293202d0a 100644 --- a/.github/workflows/lint_and_test.yml +++ b/.github/workflows/lint_and_test.yml @@ -23,7 +23,10 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: @@ -46,16 +49,21 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v3 with: node-version: '18.x' - - run: npm i && npm i -g typescript && rm -rdf ./FlowPlugins && tsc + - run: npm i && npm i -g typescript && rm -rdf ./FlowPlugins && tsc -v && tsc - uses: stefanzweifel/git-auto-commit-action@v5 + if: ${{ github.event.pull_request.head.repo.full_name == 'org/repo' }} with: - commit_message: Apply auto-build changes \ No newline at end of file + commit_message: Apply auto-build changes + + - run: | + (git diff --quiet HEAD -- 2>/dev/null && echo "No uncommitted changes" \ + || (echo "Error - Uncommitted changes found." && git --no-pager diff HEAD && exit 1)) \ No newline at end of file diff --git a/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js b/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js index c21d1ee11..cef350fa0 100644 --- a/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js +++ b/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js @@ -26,14 +26,10 @@ const details = () => ({ Settings are dependant on file bitrate working by the logic that H265 can support the same amount of data at half the bitrate of H264. This plugin will skip files already in HEVC, AV1 & VP9 unless "reconvert_hevc" is marked as true. If it is then these will be reconverted again if they exceed the bitrate specified in "hevc_max_bitrate". - This plugin will also attempt to use mkvpropedit to generate accurate bitrate metadata in MKV files. - It's not required to enable mkvpropedit but highly recommended to ensure accurate bitrates are used when - encoding your media. - \n\n==NOTE== Intel ARC cards are reportedly working successfully with this plugin, however please bare in mind that - I've not officially tested with them yet and your results might vary. Don't just assume it will work and if it does - ensure you properly test your files & workflow!`, - Version: '1.2', - Tags: 'pre-processing,ffmpeg,video only,qsv,h265,hevc,mkvpropedit,configurable', + This plugin relies on understanding the accurate video bitrate of your files. It's highly recommended to remux + into MKV & enable "Run mkvpropedit on files before running plugins" under Tdarr>Options.`, + Version: '1.3', + Tags: 'pre-processing,ffmpeg,video only,qsv,h265,hevc,configurable', Inputs: [ { name: 'container', @@ -101,7 +97,7 @@ const details = () => ({ ==DESCRIPTION== \\nSpecify if we want to enable 10bit encoding. \\nIf this is enabled files will be processed and converted into 10bit - HEVC using main10 profile and with p010le pixel format. \n + HEVC using main10 profile and with p010le pixel format.\n If you just want to retain files that are already 10 bit then this can be left as false, as 10bit to 10bit in ffmpeg should be automatic. \\n @@ -161,7 +157,6 @@ const details = () => ({ https://ffmpeg.org/ffmpeg-codecs.html#toc-HEVC-Options-1 \\n ==WARNING== \\n - Just because a cmd is mentioned doesn't mean your installed version of ffmpeg supports it... Be certain to verify the cmds work before adding to your workflow. \\n Check Tdarr Help Tab. Enter ffmpeg cmd - "-h encoder=hevc_qsv". This will give a list of supported commands. \\n MAC SPECIFIC - This option is ignored on Mac because videotoolbox is used rather than qsv. @@ -170,6 +165,7 @@ const details = () => ({ \\nDefault is empty but the first example below has a suggested value. If unsure just leave empty. \\nEnsure to only use cmds valid to encoding QSV as the script handles other ffmpeg cmds relating to bitrate etc. Anything else entered here might be supported but could cause undesired results. + \\nIf you are using a "-vf" cmd, please put it at the end to avoid issues! \\nExample:\\n -look_ahead 1 -look_ahead_depth 100 -extbrc 1 -rdo 1 -mbbrc 1 -b_strategy 1 -adaptive_i 1 -adaptive_b 1 \\n Above enables look ahead, extended bitrate control, b-frames, etc.\\n @@ -189,7 +185,7 @@ const details = () => ({ }, tooltip: `\\n ==DESCRIPTION== - \\nSpecify bitrate cutoff, files with a video bitrate lower then this will not be processed. \n + \\nSpecify bitrate cutoff, files with a video bitrate lower then this will not be processed.\n \\n ==INFO== \\nRate is in kbps. @@ -330,15 +326,13 @@ let bitrateSettings = ''; let inflatedCutoff = 0; let main10 = false; let high10 = false; +let swDecode = false; let videoBR = 0; -// Finds the first video stream and get video bitrate - // eslint-disable-next-line @typescript-eslint/no-unused-vars const plugin = (file, librarySettings, inputs, otherArguments) => { const lib = require('../methods/lib')(); const os = require('os'); - const proc = require('child_process'); // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign inputs = lib.loadDefaultValues(inputs, details); const response = { @@ -353,126 +347,71 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { if (file.fileMedium !== 'video') { response.processFile = false; - response.infoLog += `☒ File seems to be ${file.fileMedium} & not video. Exiting \n`; + response.infoLog += `☒ File seems to be ${file.fileMedium} & not video. Exiting\n`; return response; } - // MKVPROPEDIT - Refresh video stats - const intStatsDays = 7; // Use 1 week threshold for new stats - let statsUptoDate = false; - const currentFileName = file._id; - let statsError = false; - let metadataEncode = ''; - - // Only process MKV files - if (file.container === 'mkv') { - let datStats = Date.parse(new Date(70, 1).toISOString()); // Placeholder date - metadataEncode = `-map_metadata:g -1 -metadata JBDONEDATE=${datStats}`; - if (file.mediaInfo.track[0].extra !== undefined && file.mediaInfo.track[0].extra.JBDONEDATE !== undefined) { - datStats = Date.parse(file.mediaInfo.track[0].extra.JBDONEDATE); - } else { - try { - if ( - file.mediaInfo.track[0].extra !== undefined - && file.ffProbeData.streams[0].tags['_STATISTICS_WRITING_DATE_UTC-eng'] !== undefined - ) { - // Set stats date to match info inside file - datStats = Date.parse(`${file.ffProbeData.streams[0].tags['_STATISTICS_WRITING_DATE_UTC-eng']} GMT`); - } - } catch (err) { - // Catch error - Ignore & carry on - If check can bomb out if the tag doesn't exist... - } - } - - // Threshold for stats date - const statsThres = Date.parse(new Date(new Date().setDate(new Date().getDate() - intStatsDays)).toISOString()); - - // Strings for easy to read dates in info log - let statsThresString = new Date(statsThres); - statsThresString = statsThresString.toUTCString(); - let datStatsString = new Date(datStats); - datStatsString = datStatsString.toUTCString(); - response.infoLog += `Checking file stats - If stats are older than ${intStatsDays} days we'll grab new stats.\n - Stats threshold: ${statsThresString}\n - Current stats date: ${datStatsString}\n`; - - // Are the stats out of date? - if (datStats >= statsThres) { - statsUptoDate = true; - response.infoLog += '☑ File stats are upto date! - Continuing...\n'; - } else { - response.infoLog += '☒ File stats are out of date! - Will attempt to use mkvpropedit to refresh stats\n'; - try { - if (otherArguments.mkvpropeditPath !== '') { // Try to use mkvpropedit path if it is set - proc.execSync(`"${otherArguments.mkvpropeditPath}" --add-track-statistics-tags "${currentFileName}"`); - } else { // Otherwise just use standard mkvpropedit cmd - proc.execSync(`mkvpropedit --add-track-statistics-tags "${currentFileName}"`); - } - } catch (err) { - response.infoLog += '☒ Error updating file stats - Possible mkvpropedit failure or file issue - ' - + ' Ensure mkvpropedit is set correctly in the node settings & check the filename for unusual characters.\n' - + ' Continuing but file stats will likely be inaccurate...\n'; - statsError = true; - } - if (statsError !== true) { - // File now updated with new stats - response.infoLog += 'Remuxing file to write in updated file stats! \n'; - response.preset += `-fflags +genpts -map 0 -c copy -max_muxing_queue_size 9999 -map_metadata:g -1 - -metadata JBDONEDATE=${new Date().toISOString()}`; - response.processFile = true; - return response; - } - } - } else { - response.infoLog += 'Input file is not MKV so cannot use mkvpropedit to get new file stats. ' - + 'Continuing but file stats will likely be inaccurate...\n'; - } - for (let i = 0; i < file.ffProbeData.streams.length; i += 1) { const strstreamType = file.ffProbeData.streams[i].codec_type.toLowerCase(); - videoIdx = -1; // Check if stream is a video. - if (videoIdx === -1 && strstreamType === 'video') { - videoIdx = i; - videoBR = Number(file.mediaInfo.track[i + 1].BitRate) / 1000; - - // If MediaInfo fails somehow fallback to ffprobe - Try two types of tags that might exist - if (videoBR <= 0) { - if (Number(file.ffProbeData.streams[i].tags.BPS) > 0) { - videoBR = file.ffProbeData.streams[i].tags.BPS / 1000; - } else { - try { - if (Number(file.ffProbeData.streams[i].tags.BPS['-eng']) > 0) { - videoBR = file.ffProbeData.streams[i].tags.BPS['-eng'] / 1000; + if (strstreamType === 'video') { + if (file.ffProbeData.streams[i].codec_name !== 'mjpeg' + && file.ffProbeData.streams[i].codec_name !== 'png') { + if (videoBR <= 0) { // Process if videoBR is not yet valid + try { // Try checking file stats using Mediainfo first, then ffprobe. + videoBR = Number(file.mediaInfo.track[i + 1].BitRate) / 1000; + if (videoBR <= 0 || Number.isNaN(videoBR)) { + if (Number(file.ffProbeData.streams[i].tags.BPS) > 0) { + videoBR = file.ffProbeData.streams[i].tags.BPS / 1000; + } else if (Number(file.ffProbeData.streams[i].tags.BPS['-eng']) > 0) { + videoBR = file.ffProbeData.streams[i].tags.BPS['-eng'] / 1000; + } + } + } catch (err) { + // Catch error - Ignore & carry on - If check can bomb out if tags don't exist... + videoBR = 0; // Set videoBR to 0 for safety + } + } + if (duration <= 0) { // Process if duration is not yet valid + try { // Attempt to get duration info + if (Number.isNaN(file.meta.Duration)) { + duration = file.meta.Duration; + duration = (new Date(`1970-01-01T${duration}Z`).getTime() / 1000) / 60; + } else if (file.meta.Duration > 0) { + duration = file.meta.Duration / 60; + } + if (duration <= 0 || Number.isNaN(duration)) { + if (typeof file.mediaInfo.track[i + 1].Duration !== 'undefined') { + duration = file.mediaInfo.track[i + 1].Duration; + duration = (new Date(`1970-01-01T${duration}Z`).getTime() / 1000) / 60; + } else if (typeof file.ffProbeData.streams[i].tags.DURATION !== 'undefined') { + duration = file.ffProbeData.streams[i].tags.DURATION; + duration = (new Date(`1970-01-01T${duration}Z`).getTime() / 1000) / 60; + } } } catch (err) { // Catch error - Ignore & carry on - If check can bomb out if tags don't exist... + duration = 0; // Set duration to 0 for safety } } + if ((videoBR <= 0 || Number.isNaN(videoBR)) || (duration <= 0 || Number.isNaN(duration))) { + // videoBR or duration not yet valid so Loop + } else { + break;// Exit loop if both valid + } } } } - // Check if duration info is filled, if so convert time format to minutes. - // If not filled then get duration of video stream and do the same. - if (typeof file.meta.Duration !== 'undefined') { - duration = file.meta.Duration; - // Get seconds by using a Date & then convert to minutes - duration = (new Date(`1970-01-01T${duration}Z`).getTime() / 1000) / 60; - } else { - duration = file.ffProbeData.streams[videoIdx].tags.DURATION; - duration = (new Date(`1970-01-01T${duration}Z`).getTime() / 1000) / 60; - } - if (Number.isNaN(videoBR) || videoBR <= 0) { // Work out currentBitrate using "Bitrate = file size / (number of minutes * .0075)" currentBitrate = Math.round(file.file_size / (duration * 0.0075)); - response.infoLog += '==WARNING== Failed to get an accurate video bitrate, '; - response.infoLog += `falling back to old method to get OVERALL file bitrate of ${currentBitrate}kbps. `; - response.infoLog += 'Bitrate calculations for video encode will likely be inaccurate... \n'; + response.infoLog += '==WARNING== Failed to get an accurate video bitrate, ' + + `falling back to old method to get OVERALL file bitrate of ${currentBitrate}kbps. ` + + 'Bitrate calculations for video encode will likely be inaccurate...\n'; } else { currentBitrate = Math.round(videoBR); - response.infoLog += `☑ It looks like the current video bitrate is ${currentBitrate}kbps. \n`; + response.infoLog += `☑ It looks like the current video bitrate is ${currentBitrate}kbps.\n`; } // Get overall bitrate for use with HEVC reprocessing @@ -486,8 +425,8 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { // If targetBitrate or currentBitrate comes out as 0 then something // has gone wrong and bitrates could not be calculated. // Cancel plugin completely. - if (targetBitrate <= 0 || currentBitrate <= 0) { - response.infoLog += '☒ Target bitrate could not be calculated. Skipping this plugin. \n'; + if (targetBitrate <= 0 || currentBitrate <= 0 || overallBitRate <= 0) { + response.infoLog += '☒ Target bitrates could not be calculated. Skipping this plugin.\n'; return response; } @@ -495,19 +434,18 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { // has gone wrong as that is not what we want. // Cancel plugin completely. if (targetBitrate >= currentBitrate) { - response.infoLog += `☒ Target bitrate has been calculated as ${targetBitrate}kbps. This is equal or greater `; - response.infoLog += "than the current bitrate... Something has gone wrong and this shouldn't happen! " - + 'Skipping this plugin. \n'; + response.infoLog += `☒ Target bitrate has been calculated as ${targetBitrate}kbps. This is equal or greater than ` + + "the current bitrate... Something has gone wrong and this shouldn't happen! Skipping this plugin.\n"; return response; } // Ensure that bitrate_cutoff is set if reconvert_hevc is true since we need some protection against a loop // Cancel the plugin if (inputs.reconvert_hevc === true && inputs.bitrate_cutoff <= 0 && inputs.hevc_max_bitrate <= 0) { - response.infoLog += `Reconvert HEVC is ${inputs.reconvert_hevc}, however there is no bitrate cutoff `; - response.infoLog += 'or HEVC specific cutoff set so we have no way to know when to stop processing this file. \n' - + 'Either set reconvert_HEVC to false or set a bitrate cutoff and set a hevc_max_bitrate cutoff. \n' - + '☒ Skipping this plugin. \n'; + response.infoLog += `Reconvert HEVC is ${inputs.reconvert_hevc}, however there is no bitrate cutoff or HEVC ` + + 'specific cutoff set so we have no way to know when to stop processing this file.\n' + + 'Either set reconvert_HEVC to false or set a bitrate cutoff and set a hevc_max_bitrate cutoff.\n' + + '☒ Skipping this plugin.\n'; return response; } @@ -517,13 +455,13 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { // Checks if currentBitrate is below inputs.bitrate_cutoff. // If so then cancel plugin without touching original files. if (currentBitrate <= inputs.bitrate_cutoff) { - response.infoLog += `☑ Current bitrate is below set cutoff of ${inputs.bitrate_cutoff}kbps. \n` - + 'Cancelling plugin. \n'; + response.infoLog += `☑ Current bitrate is below set cutoff of ${inputs.bitrate_cutoff}kbps.\n` + + 'Cancelling plugin.\n'; return response; } // If above cutoff then carry on if (currentBitrate > inputs.bitrate_cutoff && inputs.reconvert_hevc === false) { - response.infoLog += '☒ Current bitrate appears to be above the cutoff. Need to process \n'; + response.infoLog += '☒ Current bitrate appears to be above the cutoff. Need to process\n'; } } @@ -531,8 +469,8 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { // Checks if targetBitrate is above inputs.max_average_bitrate. // If so then clamp target bitrate if (targetBitrate > inputs.max_average_bitrate) { - response.infoLog += 'Our target bitrate is above the max_average_bitrate '; - response.infoLog += `so clamping at max of ${inputs.max_average_bitrate}kbps. \n`; + response.infoLog += 'Our target bitrate is above the max_average_bitrate so clamping at max of ' + + `${inputs.max_average_bitrate}kbps.\n`; targetBitrate = Math.round(inputs.max_average_bitrate); minimumBitrate = Math.round(targetBitrate * 0.75); maximumBitrate = Math.round(targetBitrate * 1.25); @@ -544,14 +482,14 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { if (inputs.min_average_bitrate > 0) { // Exit the plugin is the cutoff is less than the min average bitrate. Most likely user error if (inputs.bitrate_cutoff < inputs.min_average_bitrate) { - response.infoLog += `☒ Bitrate cutoff ${inputs.bitrate_cutoff}k is less than the set minimum - average bitrate set of ${inputs.min_average_bitrate}kbps. We don't want this. Cancelling plugin. \n`; + response.infoLog += `☒ Bitrate cutoff ${inputs.bitrate_cutoff}k is less than the set minimum ` + + `average bitrate set of ${inputs.min_average_bitrate}kbps. We don't want this. Cancelling plugin.\n`; return response; } // Checks if inputs.bitrate_cutoff is below inputs.min_average_bitrate. // If so then set currentBitrate to the minimum allowed.) if (targetBitrate < inputs.min_average_bitrate) { - response.infoLog += `Target average bitrate clamped at min of ${inputs.min_average_bitrate}kbps. \n`; + response.infoLog += `Target average bitrate clamped at min of ${inputs.min_average_bitrate}kbps.\n`; targetBitrate = Math.round(inputs.min_average_bitrate); minimumBitrate = Math.round(targetBitrate * 0.75); maximumBitrate = Math.round(targetBitrate * 1.25); @@ -610,98 +548,92 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { // 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} `; - } - - // Check for HDR in files. If so exit plugin. We assume HDR files have bt2020 color spaces. HDR can be complicated - // and some aspects are still unsupported in ffmpeg I believe. Likely we don't want to re-encode anything HDR. - if (file.ffProbeData.streams[i].color_space === 'bt2020nc' - && file.ffProbeData.streams[i].color_transfer === 'smpte2084' - && file.ffProbeData.streams[i].color_primaries === 'bt2020') { - response.infoLog += '☒ This looks to be a HDR file. HDR files are unfortunately ' - + 'not supported by this plugin. Exiting plugin. \n\n'; - return response; - } - - // Now check if we're reprocessing HEVC files, if not then ensure we don't convert HEVC again - if (inputs.reconvert_hevc === false && (file.ffProbeData.streams[i].codec_name === 'hevc' - || file.ffProbeData.streams[i].codec_name === 'vp9' || file.ffProbeData.streams[i].codec_name === 'av1')) { - // Check if codec of stream is HEVC, VP9 or AV1 AND check if file.container matches inputs.container. - // If so nothing for plugin to do. - if ((file.ffProbeData.streams[i].codec_name === 'hevc' || file.ffProbeData.streams[i].codec_name === 'vp9' - || file.ffProbeData.streams[i].codec_name === 'av1') && file.container === inputs.container) { - response.infoLog += `☑ File is already HEVC, VP9 or AV1 & in ${inputs.container}. \n`; - return response; + extraArguments += `-map -0:v:${videoIdx} `; + } else { // Ensure to only do further checks if video stream is valid for use + // Check for HDR in files. Attempt to use same color + if ((file.ffProbeData.streams[i].color_space === 'bt2020nc' + || file.ffProbeData.streams[i].color_space === 'bt2020n') + && (file.ffProbeData.streams[i].color_transfer === 'smpte2084' + || file.ffProbeData.streams[i].color_transfer === 'arib-std-b67') + && file.ffProbeData.streams[i].color_primaries === 'bt2020') { + response.infoLog += '==WARNING== This looks to be a HDR file. HDR is supported but ' + + 'correct encoding is not guaranteed.\n'; + extraArguments += `-color_primaries ${file.ffProbeData.streams[i].color_primaries} ` + + `-color_trc ${file.ffProbeData.streams[i].color_transfer} ` + + `-colorspace ${file.ffProbeData.streams[i].color_space} `; } // Check if codec of stream is HEVC, Vp9 or AV1 - // AND check if file.container does NOT match inputs.container. - // If so remux file. - if ((file.ffProbeData.streams[i].codec_name === 'hevc' || file.ffProbeData.streams[i].codec_name === 'vp9' + // AND check if file.container does NOT match inputs.container. If so remux file. + if ((file.ffProbeData.streams[i].codec_name === 'hevc' + || file.ffProbeData.streams[i].codec_name === 'vp9' || file.ffProbeData.streams[i].codec_name === 'av1') && file.container !== inputs.container) { - response.infoLog += `☒ File is HEVC, VP9 or AV1 but is not in ${inputs.container} container. Remuxing. \n`; + response.infoLog += `☒ File is HEVC, VP9 or AV1 but is not in ${inputs.container} container. Remuxing.\n`; response.preset = ` -map 0 -c copy ${extraArguments}`; response.processFile = true; return response; } - // New logic for reprocessing HEVC. Mainly done for my own use. - // We attempt to get accurate stats earlier - If we can't we fall back onto overall bitrate - // which can be inaccurate. We may inflate the current bitrate check so we don't keep looping this logic. - } else if (inputs.reconvert_hevc === true && (file.ffProbeData.streams[i].codec_name === 'hevc' - || file.ffProbeData.streams[i].codec_name === 'vp9' || file.ffProbeData.streams[i].codec_name === 'av1')) { - if (statsUptoDate !== true) { - currentBitrate = overallBitRate; // User overall bitrate if we don't have upto date stats - response.infoLog += `☒ Unable to get accurate stats for HEVC so falling back to Overall file Bitrate. - Remux to MKV to allow generation of accurate video bitrate statistics. - File overall bitrate is ${overallBitRate}kbps.\n`; - } - if (inputs.hevc_max_bitrate > 0) { - if (currentBitrate > inputs.hevc_max_bitrate) { - // If bitrate is higher then hevc_max_bitrate then need to re-encode - response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, ` - + `VP9 or AV1. Using HEVC specific cutoff of ${inputs.hevc_max_bitrate}kbps. \n`; - response.infoLog += '☒ The file is still above this new cutoff! Reconverting. \n'; - } else { - // Otherwise we're now below the hevc cutoff and we can exit - response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, ` - + `VP9 or AV1. Using HEVC specific cutoff of ${inputs.hevc_max_bitrate}kbps. \n`; - response.infoLog += '☑ The file is NOT above this new cutoff. Exiting plugin. \n'; + // Now check if we're reprocessing HEVC files, if not then ensure we don't convert HEVC again + if (inputs.reconvert_hevc === false && (file.ffProbeData.streams[i].codec_name === 'hevc' + || file.ffProbeData.streams[i].codec_name === 'vp9' || file.ffProbeData.streams[i].codec_name === 'av1')) { + // Check if codec of stream is HEVC, VP9 or AV1 AND check if file.container matches inputs.container. + // If so nothing for plugin to do. + if ((file.ffProbeData.streams[i].codec_name === 'hevc' || file.ffProbeData.streams[i].codec_name === 'vp9' + || file.ffProbeData.streams[i].codec_name === 'av1') && file.container === inputs.container) { + response.infoLog += `☑ File is already HEVC, VP9 or AV1 & in ${inputs.container}.\n`; return response; } - // If we're not using the hevc max bitrate then we need a safety net to try and ensure we don't keep - // looping this plugin. For maximum safety we simply multiply the cutoff by 2. - } else if (currentBitrate > (inputs.bitrate_cutoff * 2)) { - inflatedCutoff = Math.round(inputs.bitrate_cutoff * 2); - response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, `; - response.infoLog += 'VP9 or AV1. Will use Overall file Bitrate for HEVC files as safety, '; - response.infoLog += `bitrate is ${overallBitRate}kbps. \n`; - response.infoLog += 'HEVC specific cutoff not set so bitrate_cutoff is multiplied by 2 for safety! \n'; - response.infoLog += `Cutoff now temporarily ${inflatedCutoff}kbps. \n`; - response.infoLog += '☒ The file is still above this new cutoff! Reconverting. \n'; - } else { - // File is below cutoff so we can exit - inflatedCutoff = Math.round(inputs.bitrate_cutoff * 2); - response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, `; - response.infoLog += 'VP9 or AV1. Will use Overall file Bitrate for HEVC files as safety, '; - response.infoLog += `bitrate is ${overallBitRate}kbps. \n`; - response.infoLog += 'HEVC specific cutoff not set so bitrate_cutoff is multiplied by 2 for safety! \n'; - response.infoLog += `Cutoff now temporarily ${inflatedCutoff}kbps. \n`; - response.infoLog += '☑The file is NOT above this new cutoff. Exiting plugin. \n'; - return response; + // New logic for reprocessing HEVC. Mainly done for my own use. + // We attempt to get accurate stats earlier - If we can't we fall back onto overall bitrate + // which can be inaccurate. We may inflate the current bitrate check so we don't keep looping this logic. + } else if (inputs.reconvert_hevc === true && (file.ffProbeData.streams[i].codec_name === 'hevc' + || file.ffProbeData.streams[i].codec_name === 'vp9' || file.ffProbeData.streams[i].codec_name === 'av1')) { + if (inputs.hevc_max_bitrate > 0) { + if (currentBitrate > inputs.hevc_max_bitrate) { + // If bitrate is higher then hevc_max_bitrate then need to re-encode + response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, VP9 or AV1. ` + + `Using HEVC specific cutoff of ${inputs.hevc_max_bitrate}kbps.\n` + + '☒ The file is still above this new cutoff! Reconverting.\n'; + } else { + // Otherwise we're now below the hevc cutoff and we can exit + response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, VP9 or AV1. ` + + `Using HEVC specific cutoff of ${inputs.hevc_max_bitrate}kbps.\n` + + '☑ The file is NOT above this new cutoff. Exiting plugin.\n'; + return response; + } + + // If we're not using the hevc max bitrate then we need a safety net to try and ensure we don't keep + // looping this plugin. For maximum safety we simply multiply the cutoff by 2. + } else if (currentBitrate > (inputs.bitrate_cutoff * 2)) { + inflatedCutoff = Math.round(inputs.bitrate_cutoff * 2); + response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, VP9 or AV1. ` + + `Will use Overall file Bitrate for HEVC files as safety, bitrate is ${overallBitRate}kbps.\n` + + 'HEVC specific cutoff not set so bitrate_cutoff is multiplied by 2 for safety!\n' + + `Cutoff now temporarily ${inflatedCutoff}kbps.\n` + + '☒ The file is still above this new cutoff! Reconverting.\n'; + } else { + // File is below cutoff so we can exit + inflatedCutoff = Math.round(inputs.bitrate_cutoff * 2); + response.infoLog += `Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, VP9 or AV1. ` + + `Will use Overall file Bitrate for HEVC files as safety, bitrate is ${overallBitRate}kbps.\n` + + 'HEVC specific cutoff not set so bitrate_cutoff is multiplied by 2 for safety!\n' + + `Cutoff now temporarily ${inflatedCutoff}kbps.\n` + + '☑The file is NOT above this new cutoff. Exiting plugin.\n'; + return response; + } } - } - // On testing I've found files in the High10 profile don't play nice with hw decoding so mark these - if (file.ffProbeData.streams[i].profile === 'High 10') { - high10 = true; - response.infoLog += 'Input file is 10bit using High10. Disabling hardware decoding to avoid problems. \n'; - } - // If files are 10 bit or the enable_10bit setting is used mark to enable Main10. - if (file.ffProbeData.streams[i].profile === 'Main 10' || file.ffProbeData.streams[i].bits_per_raw_sample === '10' - || inputs.enable_10bit === true) { - main10 = true; + // Files in the High10 profile are not supported for HW Decode + if (file.ffProbeData.streams[i].profile === 'High 10') { + high10 = true; + main10 = true; + // If files are 10 bit or the enable_10bit setting is used mark to enable Main10. + } else if (file.ffProbeData.streams[i].profile === 'Main 10' + || file.ffProbeData.streams[i].bits_per_raw_sample === '10' || inputs.enable_10bit === true) { + main10 = true; + } } // Increment video index. Needed to keep track of video id in case there is more than one video track. @@ -710,78 +642,145 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { } } + // Specify the output format + switch (inputs.container) { + case 'mkv': + extraArguments += '-f matroska '; + break; + case 'mp4': + extraArguments += '-f mp4 '; + break; + default: + } + + // Some video codecs don't support HW decode so mark these + // VC1 & VP8 are no longer supported on new HW, add cases here if your HW does support + switch (file.video_codec_name) { + case 'mpeg2': + break; + case 'h264': + if (high10 === true) { + swDecode = true; + response.infoLog += 'Input file is h264 High10. Hardware Decode not supported.\n'; + } + break; + case 'mjpeg': + break; + case 'hevc': + break; + case 'vp9':// Should be supported by 8th Gen + + break; + case 'av1':// Should be supported by 11th gen + + break; + default: + swDecode = true; + response.infoLog += `Input file is ${file.video_codec_name}. Hardware Decode not supported.\n`; + } + // Are we encoding to 10 bit? If so enable correct profile & pixel format. - if (high10 === true) { // This is used if we have High10 files. SW decode and use standard -pix_fmt p010le - extraArguments += '-profile:v main10 -pix_fmt p010le '; - response.infoLog += '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format \n'; - } else if (main10 === true) { // Pixel formate method when using HW decode - extraArguments += '-profile:v main10 -vf scale_qsv=format=p010le '; - response.infoLog += '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format \n'; + if (os.platform() !== 'darwin') { + if (swDecode === true && main10 === true) { + // This is used if we have High10 or Main10 is enabled & odd format files. + // SW decode and use standard -pix_fmt p010le + extraArguments += '-profile:v main10 -pix_fmt p010le '; + response.infoLog += '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n'; + } else if (main10 === true) { // Pixel formate method when using HW decode + if (inputs.extra_qsv_options.search('-vf scale_qsv') >= 0) { + extraArguments += '-profile:v main10'; + // eslint-disable-next-line no-param-reassign + inputs.extra_qsv_options += ',format=p010le'; // Only add on the pixel format to existing scale_qsv cmd + } else { + extraArguments += '-profile:v main10 -vf scale_qsv=format=p010le'; + } + response.infoLog += '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n'; + } + } else { + // Mac - Video toolbox profile & pixel format + extraArguments += '-profile:v 2 -pix_fmt yuv420p10le '; + response.infoLog += '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n'; } // Set bitrateSettings variable using bitrate information calculated 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 += 'Encode variable bitrate settings: \n'; - response.infoLog += `Target = ${targetBitrate}k \n`; - response.infoLog += `Minimum = ${minimumBitrate}k \n`; - response.infoLog += `Maximum = ${maximumBitrate}k \n`; + response.infoLog += `Container for output selected as ${inputs.container}.\n` + + 'Encode variable bitrate settings:\n' + + `Target = ${targetBitrate}k\n` + + `Minimum = ${minimumBitrate}k\n` + + `Maximum = ${maximumBitrate}k\n`; // START PRESET // -fflags +genpts should regenerate timestamps if they end up missing... response.preset = '-fflags +genpts '; - // HW ACCEL FLAGS - I think these are good practice but are they necessary? + // HW ACCEL FLAGS // Account for different OS - if (high10 === false) { - // Seems incoming High10 files don't play nice decoding so use software decode + if (swDecode !== true) { + // Only enable hw decode for accepted formats switch (os.platform()) { case 'darwin': // Mac OS - Enable videotoolbox instead of QSV response.preset += '-hwaccel videotoolbox'; break; case 'linux': // Linux - Full device, should fix child_device_type warnings - response.preset += `-hwaccel qsv -hwaccel_output_format qsv - -init_hw_device qsv:hw_any,child_device_type=vaapi `; + response.preset += '-hwaccel qsv -hwaccel_output_format qsv ' + + '-init_hw_device qsv:hw_any,child_device_type=vaapi '; break; case 'win32': // Windows - Full device, should fix child_device_type warnings - response.preset += `-hwaccel qsv -hwaccel_output_format qsv - -init_hw_device qsv:hw_any,child_device_type=d3d11va `; + response.preset += '-hwaccel qsv -hwaccel_output_format qsv ' + + '-init_hw_device qsv:hw,child_device_type=d3d11va '; break; default: response.preset += '-hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any '; } + } else { + switch (os.platform()) { + case 'darwin': // Mac OS - Enable videotoolbox instead of QSV + response.preset += '-hwaccel videotoolbox'; + break; + case 'linux': // Linux - Full device, should fix child_device_type warnings + response.preset += '-hwaccel_output_format qsv ' + + '-init_hw_device qsv:hw_any,child_device_type=vaapi '; + break; + case 'win32': // Windows - Full device, should fix child_device_type warnings + response.preset += '-hwaccel_output_format qsv ' + + '-init_hw_device qsv:hw,child_device_type=d3d11va '; + break; + default: + // Default to enabling hwaccel for output only + response.preset += '-hwaccel_output_format qsv -init_hw_device qsv:hw_any '; + } } // DECODE FLAGS + // VC1 & VP8 are no longer supported on new HW, add cases here if your HW does support if (os.platform() !== 'darwin') { - if (high10 === false) { // Don't enable for High10 - switch (file.video_codec_name) { - case 'mpeg2': - response.preset += '-c:v mpeg2_qsv'; - break; - case 'h264': + switch (file.video_codec_name) { + case 'mpeg2': + response.preset += '-c:v mpeg2_qsv'; + break; + case 'h264': + if (high10 !== true) { // Don't enable for High10 response.preset += '-c:v h264_qsv'; - break; - case 'vc1': - response.preset += '-c:v vc1_qsv'; - break; - case 'mjpeg': - response.preset += '-c:v mjpeg_qsv'; - break; - case 'vp8': - response.preset += '-c:v vp8_qsv'; - break; - case 'hevc': - response.preset += '-c:v hevc_qsv'; - break; - case 'vp9': // Should be supported by 8th Gen + - response.preset += '-c:v vp9_qsv'; - break; - default: - response.preset += ''; - } + } else { + response.preset += `-c:v ${file.video_codec_name}`; + } + break; + case 'mjpeg': + response.preset += '-c:v mjpeg_qsv'; + break; + case 'hevc': + response.preset += '-c:v hevc_qsv'; + break; + case 'vp9': // Should be supported by 8th Gen + + response.preset += '-c:v vp9_qsv'; + break; + case 'av1': // Should be supported by 11th gen + + response.preset += '-c:v av1_qsv'; + break; + default: + // Use incoming format for software decode + response.preset += `-c:v ${file.video_codec_name}`; } } @@ -798,13 +797,94 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { response.preset += 'hevc_qsv'; break; case 'win32': - response.preset += 'hevc_qsv -load_plugin hevc_hw'; - // Windows needs the additional -load_plugin. Tested working on a Win 10 - i5-10505 + response.preset += 'hevc_qsv'; + // Tested working on a Win 10 - i5-10505 break; default: response.preset += 'hevc_qsv'; // Default to QSV } + // Only add on for HW decoded formats + // VC1 & VP8 are no longer supported on new HW, add cases here if your HW does support + if (swDecode !== true && os.platform() !== 'darwin') { + // Check if -vf cmd has already been used on user input + if (inputs.extra_qsv_options.search('-vf scale_qsv') >= 0) { + switch (file.video_codec_name) { + case 'mpeg2': + // eslint-disable-next-line no-param-reassign + inputs.extra_qsv_options += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'h264': + // eslint-disable-next-line no-param-reassign + inputs.extra_qsv_options += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'mjpeg': + // eslint-disable-next-line no-param-reassign + inputs.extra_qsv_options += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'hevc': + // eslint-disable-next-line no-param-reassign + inputs.extra_qsv_options += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'vp9': // Should be supported by 8th Gen + + // eslint-disable-next-line no-param-reassign + inputs.extra_qsv_options += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'av1': // Should be supported by 11th gen + + // eslint-disable-next-line no-param-reassign + inputs.extra_qsv_options += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + default: + } + } else if (extraArguments.search('-vf') === -1) { + // Check if -vf cmd has been used on the other var instead, if not add it & rest of cmd + switch (file.video_codec_name) { + case 'mpeg2': + extraArguments += '-vf hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'h264': + extraArguments += '-vf hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'mjpeg': + extraArguments += '-vf hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'hevc': + extraArguments += '-vf hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'vp9': // Should be supported by 8th Gen + + extraArguments += '-vf hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'av1': // Should be supported by 11th gen + + extraArguments += '-vf hwupload=extra_hw_frames=64,format=qsv '; + break; + default: + } + } else { + // Otherwise add the cmd onto the end + switch (file.video_codec_name) { + case 'mpeg2': + extraArguments += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'h264': + extraArguments += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'mjpeg': + extraArguments += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'hevc': + extraArguments += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'vp9': // Should be supported by 8th Gen + + extraArguments += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + case 'av1': // Should be supported by 11th gen + + extraArguments += ',hwupload=extra_hw_frames=64,format=qsv '; + break; + default: + } + } + } + // Add the rest of the ffmpeg command switch (os.platform()) { case 'darwin': @@ -818,11 +898,11 @@ const plugin = (file, librarySettings, inputs, otherArguments) => { // Normal behavior response.preset += ` ${bitrateSettings} ` + `-preset ${inputs.encoder_speedpreset} ${inputs.extra_qsv_options} ` - + `-c:a copy -c:s copy -max_muxing_queue_size 9999 ${extraArguments} ${metadataEncode}`; + + `-c:a copy -c:s copy -max_muxing_queue_size 9999 ${extraArguments}`; } response.processFile = true; - response.infoLog += 'File Transcoding... \n'; + response.infoLog += 'File Transcoding...\n'; return response; }; diff --git a/FlowPlugins/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.js index 5e1449e3a..4ba49f96b 100644 --- a/FlowPlugins/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.js +++ b/FlowPlugins/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.js @@ -16,6 +16,66 @@ var details = function () { return ({ sidebarPosition: -1, icon: '', inputs: [ + { + label: 'Use % of Input Bitrate', + name: 'useInputBitrate', + type: 'boolean', + defaultValue: 'false', + inputUI: { + type: 'switch', + }, + tooltip: 'Specify whether to use a % of input bitrate as the output bitrate', + }, + { + label: 'Target Bitrate %', + name: 'targetBitratePercent', + type: 'string', + defaultValue: '50', + inputUI: { + type: 'text', + displayConditions: { + logic: 'AND', + sets: [ + { + logic: 'AND', + inputs: [ + { + name: 'useInputBitrate', + value: 'true', + condition: '===', + }, + ], + }, + ], + }, + }, + tooltip: 'Specify the target bitrate as a % of the input bitrate', + }, + { + label: 'Fallback Bitrate', + name: 'fallbackBitrate', + type: 'string', + defaultValue: '4000', + inputUI: { + type: 'text', + displayConditions: { + logic: 'AND', + sets: [ + { + logic: 'AND', + inputs: [ + { + name: 'useInputBitrate', + value: 'true', + condition: '===', + }, + ], + }, + ], + }, + }, + tooltip: 'Specify fallback bitrate in kbps if input bitrate is not available', + }, { label: 'Bitrate', name: 'bitrate', @@ -23,6 +83,21 @@ var details = function () { return ({ defaultValue: '5000', inputUI: { type: 'text', + displayConditions: { + logic: 'AND', + sets: [ + { + logic: 'AND', + inputs: [ + { + name: 'useInputBitrate', + value: 'true', + condition: '!==', + }, + ], + }, + ], + }, }, tooltip: 'Specify bitrate in kbps', }, @@ -40,10 +115,36 @@ var plugin = function (args) { var lib = require('../../../../../methods/lib')(); // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign args.inputs = lib.loadDefaultValues(args.inputs, details); + var useInputBitrate = args.inputs.useInputBitrate; + var targetBitratePercent = String(args.inputs.targetBitratePercent); + var fallbackBitrate = String(args.inputs.fallbackBitrate); + var bitrate = String(args.inputs.bitrate); args.variables.ffmpegCommand.streams.forEach(function (stream) { + var _a, _b, _c, _d; if (stream.codec_type === 'video') { var ffType = (0, fileUtils_1.getFfType)(stream.codec_type); - stream.outputArgs.push("-b:".concat(ffType, ":{outputTypeIndex}"), "".concat(String(args.inputs.bitrate), "k")); + if (useInputBitrate) { + args.jobLog('Attempting to use % of input bitrate as output bitrate'); + // check if input bitrate is available + var mediainfoIndex = stream.index + 1; + var inputBitrate = (_d = (_c = (_b = (_a = args === null || args === void 0 ? void 0 : args.inputFileObj) === null || _a === void 0 ? void 0 : _a.mediaInfo) === null || _b === void 0 ? void 0 : _b.track) === null || _c === void 0 ? void 0 : _c[mediainfoIndex]) === null || _d === void 0 ? void 0 : _d.BitRate; + if (inputBitrate) { + args.jobLog("Found input bitrate: ".concat(inputBitrate)); + // @ts-expect-error type + inputBitrate = parseInt(inputBitrate, 10) / 1000; + var targetBitrate = (inputBitrate * (parseInt(targetBitratePercent, 10) / 100)); + args.jobLog("Setting video bitrate as ".concat(targetBitrate, "k")); + stream.outputArgs.push("-b:".concat(ffType, ":{outputTypeIndex}"), "".concat(targetBitrate, "k")); + } + else { + args.jobLog("Unable to find input bitrate, setting fallback bitrate as ".concat(fallbackBitrate, "k")); + stream.outputArgs.push("-b:".concat(ffType, ":{outputTypeIndex}"), "".concat(fallbackBitrate, "k")); + } + } + else { + args.jobLog("Using fixed bitrate. Setting video bitrate as ".concat(bitrate, "k")); + stream.outputArgs.push("-b:".concat(ffType, ":{outputTypeIndex}"), "".concat(bitrate, "k")); + } } }); return { diff --git a/FlowPlugins/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.js index c38a7e654..c0adfb2fa 100644 --- a/FlowPlugins/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.js +++ b/FlowPlugins/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.js @@ -45,8 +45,8 @@ var plugin = function (args) { // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign args.inputs = lib.loadDefaultValues(args.inputs, details); var extensions = String(args.inputs.extensions); - var extensionArray = extensions.trim().split(','); - var extension = (0, fileUtils_1.getContainer)(args.inputFileObj._id); + var extensionArray = extensions.trim().split(',').map(function (row) { return row.toLowerCase(); }); + var extension = (0, fileUtils_1.getContainer)(args.inputFileObj._id).toLowerCase(); var extensionMatch = false; if (extensionArray.includes(extension)) { extensionMatch = true; diff --git a/FlowPlugins/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.js index 87f44da62..271ef8b78 100644 --- a/FlowPlugins/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.js +++ b/FlowPlugins/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.js @@ -20,12 +20,10 @@ var details = function () { return ({ label: 'Terms', name: 'terms', type: 'string', - // eslint-disable-next-line no-template-curly-in-string defaultValue: '_720p,_1080p', inputUI: { type: 'text', }, - // eslint-disable-next-line no-template-curly-in-string tooltip: 'Specify terms to check for in file name using comma seperated list e.g. _720p,_1080p', }, ], diff --git a/FlowPlugins/CommunityFlowPlugins/file/checkFileNameIncludes/2.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/file/checkFileNameIncludes/2.0.0/index.js new file mode 100644 index 000000000..8bad2380f --- /dev/null +++ b/FlowPlugins/CommunityFlowPlugins/file/checkFileNameIncludes/2.0.0/index.js @@ -0,0 +1,93 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.plugin = exports.details = void 0; +var fileUtils_1 = require("../../../../FlowHelpers/1.0.0/fileUtils"); +/* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */ +var details = function () { return ({ + name: 'Check File Name Includes', + description: 'Check if a file name includes specific terms. Only needs to match one term', + style: { + borderColor: 'orange', + }, + tags: 'video', + isStartPlugin: false, + pType: '', + requiresVersion: '2.11.01', + sidebarPosition: -1, + icon: 'faQuestion', + inputs: [ + { + label: 'Terms', + name: 'terms', + type: 'string', + defaultValue: '_720p,_1080p', + inputUI: { + type: 'text', + }, + tooltip: 'Specify terms to check for in file name using comma seperated list e.g. _720p,_1080p', + }, + { + label: 'Pattern (regular expression)', + name: 'pattern', + type: 'string', + defaultValue: '', + inputUI: { + type: 'text', + }, + tooltip: 'Specify the pattern (regex) to check for in file name e.g. ^Pattern.*mkv$', + }, + { + label: 'Include file directory in check', + name: 'includeFileDirectory', + type: 'boolean', + defaultValue: 'false', + inputUI: { + type: 'switch', + }, + tooltip: 'Should the terms and patterns be evaluated against the file directory e.g. false, true', + }, + ], + outputs: [ + { + number: 1, + tooltip: 'File name contains terms or patterns', + }, + { + number: 2, + tooltip: 'File name does not contain any of the terms or patterns', + }, + ], +}); }; +exports.details = details; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +var plugin = function (args) { + var lib = require('../../../../../methods/lib')(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign + args.inputs = lib.loadDefaultValues(args.inputs, details); + var terms = String(args.inputs.terms); + var pattern = String(args.inputs.pattern); + var includeFileDirectory = args.inputs.includeFileDirectory; + var fileName = includeFileDirectory + ? args.inputFileObj._id + : "".concat((0, fileUtils_1.getFileName)(args.inputFileObj._id), ".").concat((0, fileUtils_1.getContainer)(args.inputFileObj._id)); + var searchCriteriasArray = terms.trim().split(',') + .map(function (term) { return term.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); }); // https://github.com/tc39/proposal-regex-escaping + if (pattern) { + searchCriteriasArray.push(pattern); + } + var searchCriteriaMatched = searchCriteriasArray + .find(function (searchCriteria) { return new RegExp(searchCriteria).test(fileName); }); + var isAMatch = searchCriteriaMatched !== undefined; + if (isAMatch) { + args.jobLog("'".concat(fileName, "' includes '").concat(searchCriteriaMatched, "'")); + } + else { + args.jobLog("'".concat(fileName, "' does not include any of the terms or patterns")); + } + return { + outputFileObj: args.inputFileObj, + outputNumber: isAMatch ? 1 : 2, + variables: args.variables, + }; +}; +exports.plugin = plugin; diff --git a/FlowPlugins/CommunityFlowPlugins/file/copyMoveFolderContent/1.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/file/copyMoveFolderContent/1.0.0/index.js index e734badd2..4ac05b361 100644 --- a/FlowPlugins/CommunityFlowPlugins/file/copyMoveFolderContent/1.0.0/index.js +++ b/FlowPlugins/CommunityFlowPlugins/file/copyMoveFolderContent/1.0.0/index.js @@ -150,33 +150,31 @@ var details = function () { return ({ ], }); }; exports.details = details; -var doOperation = function (_a) { - var args = _a.args, sourcePath = _a.sourcePath, destinationPath = _a.destinationPath, operation = _a.operation; - return __awaiter(void 0, void 0, void 0, function () { - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - args.jobLog("Input path: ".concat(sourcePath)); - args.jobLog("Output path: ".concat(destinationPath)); - if (!(sourcePath === destinationPath)) return [3 /*break*/, 1]; - args.jobLog("Input and output path are the same, skipping ".concat(operation)); - return [3 /*break*/, 3]; - case 1: - args.deps.fsextra.ensureDirSync((0, fileUtils_1.getFileAbosluteDir)(destinationPath)); - return [4 /*yield*/, (0, fileMoveOrCopy_1.default)({ - operation: operation, - sourcePath: sourcePath, - destinationPath: destinationPath, - args: args, - })]; - case 2: - _b.sent(); - _b.label = 3; - case 3: return [2 /*return*/]; - } - }); +var doOperation = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var args = _b.args, sourcePath = _b.sourcePath, destinationPath = _b.destinationPath, operation = _b.operation; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + args.jobLog("Input path: ".concat(sourcePath)); + args.jobLog("Output path: ".concat(destinationPath)); + if (!(sourcePath === destinationPath)) return [3 /*break*/, 1]; + args.jobLog("Input and output path are the same, skipping ".concat(operation)); + return [3 /*break*/, 3]; + case 1: + args.deps.fsextra.ensureDirSync((0, fileUtils_1.getFileAbosluteDir)(destinationPath)); + return [4 /*yield*/, (0, fileMoveOrCopy_1.default)({ + operation: operation, + sourcePath: sourcePath, + destinationPath: destinationPath, + args: args, + })]; + case 2: + _c.sent(); + _c.label = 3; + case 3: return [2 /*return*/]; + } }); -}; +}); }; // eslint-disable-next-line @typescript-eslint/no-unused-vars var plugin = function (args) { return __awaiter(void 0, void 0, void 0, function () { var lib, _a, keepRelativePath, allFiles, sourceDirectory, outputDirectory, copyOrMove, fileExtensions, outputPath, subStem, sourceDir, filesInDir, i; diff --git a/FlowPlugins/CommunityFlowPlugins/tools/applyRadarrOrSonarrNamingPolicy/1.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/tools/applyRadarrOrSonarrNamingPolicy/1.0.0/index.js new file mode 100644 index 000000000..dfe4ac171 --- /dev/null +++ b/FlowPlugins/CommunityFlowPlugins/tools/applyRadarrOrSonarrNamingPolicy/1.0.0/index.js @@ -0,0 +1,288 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.plugin = exports.details = void 0; +var fileMoveOrCopy_1 = __importDefault(require("../../../../FlowHelpers/1.0.0/fileMoveOrCopy")); +var fileUtils_1 = require("../../../../FlowHelpers/1.0.0/fileUtils"); +var details = function () { return ({ + name: 'Apply Radarr or Sonarr naming policy', + description: 'Apply Radarr or Sonarr naming policy to a file. This plugin should be called after the original file has been ' + + 'replaced and Radarr or Sonarr has been notified. Radarr or Sonarr should also be notified after this plugin.', + style: { + borderColor: 'green', + }, + tags: '', + isStartPlugin: false, + pType: '', + requiresVersion: '2.11.01', + sidebarPosition: -1, + icon: 'faPenToSquare', + inputs: [ + { + label: 'Arr', + name: 'arr', + type: 'string', + defaultValue: 'radarr', + inputUI: { + type: 'dropdown', + options: ['radarr', 'sonarr'], + }, + tooltip: 'Specify which arr to use', + }, + { + label: 'Arr API Key', + name: 'arr_api_key', + type: 'string', + defaultValue: '', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr api key here', + }, + { + label: 'Arr Host', + name: 'arr_host', + type: 'string', + defaultValue: 'http://192.168.1.1:7878', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr host here.' + + '\\nExample:\\n' + + 'http://192.168.1.1:7878\\n' + + 'http://192.168.1.1:8989\\n' + + 'https://radarr.domain.com\\n' + + 'https://sonarr.domain.com\\n', + }, + ], + outputs: [ + { + number: 1, + tooltip: 'Radarr or Sonarr notified', + }, + { + number: 2, + tooltip: 'Radarr or Sonarr do not know this file', + }, + ], +}); }; +exports.details = details; +var getFileInfoFromLookup = function (args, arrApp, fileName) { return __awaiter(void 0, void 0, void 0, function () { + var fInfo, imdbId, lookupResponse; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + fInfo = { id: '-1' }; + imdbId = (_b = (_a = /\b(tt|nm|co|ev|ch|ni)\d{7,10}?\b/i.exec(fileName)) === null || _a === void 0 ? void 0 : _a.at(0)) !== null && _b !== void 0 ? _b : ''; + if (!(imdbId !== '')) return [3 /*break*/, 2]; + return [4 /*yield*/, args.deps.axios({ + method: 'get', + url: "".concat(arrApp.host, "/api/v3/").concat(arrApp.name === 'radarr' ? 'movie' : 'series', "/lookup?term=imdb:").concat(imdbId), + headers: arrApp.headers, + })]; + case 1: + lookupResponse = _c.sent(); + fInfo = arrApp.delegates.getFileInfoFromLookupResponse(lookupResponse, fileName); + args.jobLog("".concat(arrApp.content, " ").concat(fInfo.id !== '-1' ? "'".concat(fInfo.id, "' found") : 'not found') + + " for imdb '".concat(imdbId, "'")); + _c.label = 2; + case 2: return [2 /*return*/, fInfo]; + } + }); +}); }; +var getFileInfoFromParse = function (args, arrApp, fileName) { return __awaiter(void 0, void 0, void 0, function () { + var fInfo, parseResponse; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + fInfo = { id: '-1' }; + return [4 /*yield*/, args.deps.axios({ + method: 'get', + url: "".concat(arrApp.host, "/api/v3/parse?title=").concat(encodeURIComponent((0, fileUtils_1.getFileName)(fileName))), + headers: arrApp.headers, + })]; + case 1: + parseResponse = _a.sent(); + fInfo = arrApp.delegates.getFileInfoFromParseResponse(parseResponse); + args.jobLog("".concat(arrApp.content, " ").concat(fInfo.id !== '-1' ? "'".concat(fInfo.id, "' found") : 'not found') + + " for '".concat((0, fileUtils_1.getFileName)(fileName), "'")); + return [2 /*return*/, fInfo]; + } + }); +}); }; +var getFileInfo = function (args, arrApp, fileName) { return __awaiter(void 0, void 0, void 0, function () { + var fInfo; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getFileInfoFromLookup(args, arrApp, fileName)]; + case 1: + fInfo = _a.sent(); + return [2 /*return*/, (fInfo.id === '-1' || (arrApp.name === 'sonarr' && (fInfo.seasonNumber === -1 || fInfo.episodeNumber === -1))) + ? getFileInfoFromParse(args, arrApp, fileName) + : fInfo]; + } + }); +}); }; +var plugin = function (args) { return __awaiter(void 0, void 0, void 0, function () { + var lib, newPath, isSuccessful, arr, arr_host, arrHost, originalFileName, currentFileName, headers, arrApp, fInfo, previewRenameRequestResult, fileToRename; + var _a, _b, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + lib = require('../../../../../methods/lib')(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign + args.inputs = lib.loadDefaultValues(args.inputs, details); + newPath = ''; + isSuccessful = false; + arr = String(args.inputs.arr); + arr_host = String(args.inputs.arr_host).trim(); + arrHost = arr_host.endsWith('/') ? arr_host.slice(0, -1) : arr_host; + originalFileName = (_b = (_a = args.originalLibraryFile) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : ''; + currentFileName = (_d = (_c = args.inputFileObj) === null || _c === void 0 ? void 0 : _c._id) !== null && _d !== void 0 ? _d : ''; + headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': String(args.inputs.arr_api_key), + Accept: 'application/json', + }; + arrApp = arr === 'radarr' + ? { + name: arr, + host: arrHost, + headers: headers, + content: 'Movie', + delegates: { + getFileInfoFromLookupResponse: function (lookupResponse) { var _a, _b, _c; return ({ id: String((_c = (_b = (_a = lookupResponse === null || lookupResponse === void 0 ? void 0 : lookupResponse.data) === null || _a === void 0 ? void 0 : _a.at(0)) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : -1) }); }, + getFileInfoFromParseResponse: function (parseResponse) { var _a, _b, _c; return ({ id: String((_c = (_b = (_a = parseResponse === null || parseResponse === void 0 ? void 0 : parseResponse.data) === null || _a === void 0 ? void 0 : _a.movie) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : -1) }); }, + buildPreviewRenameResquestUrl: function (fInfo) { return "".concat(arrHost, "/api/v3/rename?movieId=").concat(fInfo.id); }, + getFileToRenameFromPreviewRenameResponse: function (previewRenameResponse) { var _a; return (_a = previewRenameResponse.data) === null || _a === void 0 ? void 0 : _a.at(0); }, + }, + } + : { + name: arr, + host: arrHost, + headers: headers, + content: 'Serie', + delegates: { + getFileInfoFromLookupResponse: function (lookupResponse, fileName) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + var fInfo = { id: String((_c = (_b = (_a = lookupResponse === null || lookupResponse === void 0 ? void 0 : lookupResponse.data) === null || _a === void 0 ? void 0 : _a.at(0)) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : -1) }; + if (fInfo.id !== '-1') { + var seasonEpisodenumber = (_e = (_d = /\bS\d{1,3}E\d{1,4}\b/i.exec(fileName)) === null || _d === void 0 ? void 0 : _d.at(0)) !== null && _e !== void 0 ? _e : ''; + var episodeNumber = (_g = (_f = /\d{1,4}$/i.exec(seasonEpisodenumber)) === null || _f === void 0 ? void 0 : _f.at(0)) !== null && _g !== void 0 ? _g : ''; + fInfo.seasonNumber = Number((_j = (_h = /\d{1,3}/i + .exec(seasonEpisodenumber.slice(0, -episodeNumber.length))) === null || _h === void 0 ? void 0 : _h.at(0)) !== null && _j !== void 0 ? _j : '-1'); + fInfo.episodeNumber = Number(episodeNumber !== '' ? episodeNumber : -1); + } + return fInfo; + }, + getFileInfoFromParseResponse: function (parseResponse) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; + return ({ + id: String((_c = (_b = (_a = parseResponse === null || parseResponse === void 0 ? void 0 : parseResponse.data) === null || _a === void 0 ? void 0 : _a.series) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : -1), + seasonNumber: (_f = (_e = (_d = parseResponse === null || parseResponse === void 0 ? void 0 : parseResponse.data) === null || _d === void 0 ? void 0 : _d.parsedEpisodeInfo) === null || _e === void 0 ? void 0 : _e.seasonNumber) !== null && _f !== void 0 ? _f : 1, + episodeNumber: (_k = (_j = (_h = (_g = parseResponse === null || parseResponse === void 0 ? void 0 : parseResponse.data) === null || _g === void 0 ? void 0 : _g.parsedEpisodeInfo) === null || _h === void 0 ? void 0 : _h.episodeNumbers) === null || _j === void 0 ? void 0 : _j.at(0)) !== null && _k !== void 0 ? _k : 1, + }); + }, + buildPreviewRenameResquestUrl: function (fInfo) { return "".concat(arrHost, "/api/v3/rename?seriesId=").concat(fInfo.id, "&seasonNumber=").concat(fInfo.seasonNumber); }, + getFileToRenameFromPreviewRenameResponse: function (previewRenameResponse, fInfo) { + var _a; + return (_a = previewRenameResponse.data) === null || _a === void 0 ? void 0 : _a.find(function (episodeFile) { var _a; return ((_a = episodeFile.episodeNumbers) === null || _a === void 0 ? void 0 : _a.at(0)) === fInfo.episodeNumber; }); + }, + }, + }; + args.jobLog('Going to apply new name'); + args.jobLog("Renaming ".concat(arrApp.name, "...")); + return [4 /*yield*/, getFileInfo(args, arrApp, originalFileName)]; + case 1: + fInfo = _e.sent(); + if (!(fInfo.id === '-1' && currentFileName !== originalFileName)) return [3 /*break*/, 3]; + return [4 /*yield*/, getFileInfo(args, arrApp, currentFileName)]; + case 2: + fInfo = _e.sent(); + _e.label = 3; + case 3: + if (!(fInfo.id !== '-1')) return [3 /*break*/, 7]; + return [4 /*yield*/, args.deps.axios({ + method: 'get', + url: arrApp.delegates.buildPreviewRenameResquestUrl(fInfo), + headers: headers, + })]; + case 4: + previewRenameRequestResult = _e.sent(); + fileToRename = arrApp.delegates + .getFileToRenameFromPreviewRenameResponse(previewRenameRequestResult, fInfo); + if (!(fileToRename !== undefined)) return [3 /*break*/, 6]; + newPath = "".concat((0, fileUtils_1.getFileAbosluteDir)(currentFileName), "/").concat((0, fileUtils_1.getFileName)(fileToRename.newPath), ".").concat((0, fileUtils_1.getContainer)(fileToRename.newPath)); + return [4 /*yield*/, (0, fileMoveOrCopy_1.default)({ + operation: 'move', + sourcePath: currentFileName, + destinationPath: newPath, + args: args, + })]; + case 5: + isSuccessful = _e.sent(); + return [3 /*break*/, 7]; + case 6: + isSuccessful = true; + args.jobLog('✔ No rename necessary.'); + _e.label = 7; + case 7: return [2 /*return*/, { + outputFileObj: isSuccessful && newPath !== '' + ? __assign(__assign({}, args.inputFileObj), { _id: newPath }) : args.inputFileObj, + outputNumber: isSuccessful ? 1 : 2, + variables: args.variables, + }]; + } + }); +}); }; +exports.plugin = plugin; diff --git a/FlowPlugins/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.js index bae9545bf..7ef26ce87 100644 --- a/FlowPlugins/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.js +++ b/FlowPlugins/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.js @@ -47,7 +47,7 @@ var details = function () { return ({ inputUI: { type: 'text', }, - tooltip: 'Value of variable to check', + tooltip: "Value of variable to check. \nYou can specify multiple values separated by comma. For example: value1,value2,value3", }, ], outputs: [ @@ -99,23 +99,24 @@ var plugin = function (args) { } targetValue = String(targetValue); var outputNumber = 1; + var valuesArr = value.trim().split(','); if (condition === '==') { - if (targetValue === value) { - args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " matches condition ").concat(condition, " ").concat(value)); + if (valuesArr.includes(targetValue)) { + args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " matches condition ").concat(condition, " ").concat(valuesArr)); outputNumber = 1; } else { - args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " does not match condition ").concat(condition, " ").concat(value)); + args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " does not match condition ").concat(condition, " ").concat(valuesArr)); outputNumber = 2; } } else if (condition === '!=') { - if (targetValue !== value) { - args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " matches condition ").concat(condition, " ").concat(value)); + if (!valuesArr.includes(targetValue)) { + args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " matches condition ").concat(condition, " ").concat(valuesArr)); outputNumber = 1; } else { - args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " does not match condition ").concat(condition, " ").concat(value)); + args.jobLog("Variable ".concat(variable, " of value ").concat(targetValue, " does not match condition ").concat(condition, " ").concat(valuesArr)); outputNumber = 2; } } diff --git a/FlowPlugins/CommunityFlowPlugins/tools/notifyRadarrOrSonarr/2.0.0/index.js b/FlowPlugins/CommunityFlowPlugins/tools/notifyRadarrOrSonarr/2.0.0/index.js new file mode 100644 index 000000000..8b0379f3d --- /dev/null +++ b/FlowPlugins/CommunityFlowPlugins/tools/notifyRadarrOrSonarr/2.0.0/index.js @@ -0,0 +1,215 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.plugin = exports.details = void 0; +var fileUtils_1 = require("../../../../FlowHelpers/1.0.0/fileUtils"); +var details = function () { return ({ + name: 'Notify Radarr or Sonarr', + description: 'Notify Radarr or Sonarr to refresh after file change', + style: { + borderColor: 'green', + }, + tags: '', + isStartPlugin: false, + pType: '', + requiresVersion: '2.11.01', + sidebarPosition: -1, + icon: 'faBell', + inputs: [ + { + label: 'Arr', + name: 'arr', + type: 'string', + defaultValue: 'radarr', + inputUI: { + type: 'dropdown', + options: ['radarr', 'sonarr'], + }, + tooltip: 'Specify which arr to use', + }, + { + label: 'Arr API Key', + name: 'arr_api_key', + type: 'string', + defaultValue: '', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr api key here', + }, + { + label: 'Arr Host', + name: 'arr_host', + type: 'string', + defaultValue: 'http://192.168.1.1:7878', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr host here.' + + '\\nExample:\\n' + + 'http://192.168.1.1:7878\\n' + + 'http://192.168.1.1:8989\\n' + + 'https://radarr.domain.com\\n' + + 'https://sonarr.domain.com\\n', + }, + ], + outputs: [ + { + number: 1, + tooltip: 'Radarr or Sonarr notified', + }, + { + number: 2, + tooltip: 'Radarr or Sonarr do not know this file', + }, + ], +}); }; +exports.details = details; +var getId = function (args, arrApp, fileName) { return __awaiter(void 0, void 0, void 0, function () { + var imdbId, id, _a, _b, _c, _d; + var _e, _f, _g, _h, _j; + return __generator(this, function (_k) { + switch (_k.label) { + case 0: + imdbId = (_f = (_e = /\b(tt|nm|co|ev|ch|ni)\d{7,10}?\b/i.exec(fileName)) === null || _e === void 0 ? void 0 : _e.at(0)) !== null && _f !== void 0 ? _f : ''; + if (!(imdbId !== '')) return [3 /*break*/, 2]; + _b = Number; + return [4 /*yield*/, args.deps.axios({ + method: 'get', + url: "".concat(arrApp.host, "/api/v3/").concat(arrApp.name === 'radarr' ? 'movie' : 'series', "/lookup?term=imdb:").concat(imdbId), + headers: arrApp.headers, + })]; + case 1: + _a = _b.apply(void 0, [(_j = (_h = (_g = (_k.sent()).data) === null || _g === void 0 ? void 0 : _g.at(0)) === null || _h === void 0 ? void 0 : _h.id) !== null && _j !== void 0 ? _j : -1]); + return [3 /*break*/, 3]; + case 2: + _a = -1; + _k.label = 3; + case 3: + id = _a; + args.jobLog("".concat(arrApp.content, " ").concat(id !== -1 ? "'".concat(id, "' found") : 'not found', " for imdb '").concat(imdbId, "'")); + if (!(id === -1)) return [3 /*break*/, 5]; + _d = (_c = arrApp.delegates).getIdFromParseResponse; + return [4 /*yield*/, args.deps.axios({ + method: 'get', + url: "".concat(arrApp.host, "/api/v3/parse?title=").concat(encodeURIComponent((0, fileUtils_1.getFileName)(fileName))), + headers: arrApp.headers, + })]; + case 4: + id = _d.apply(_c, [(_k.sent())]); + args.jobLog("".concat(arrApp.content, " ").concat(id !== -1 ? "'".concat(id, "' found") : 'not found', " for '").concat((0, fileUtils_1.getFileName)(fileName), "'")); + _k.label = 5; + case 5: return [2 /*return*/, id]; + } + }); +}); }; +var plugin = function (args) { return __awaiter(void 0, void 0, void 0, function () { + var lib, refreshed, arr, arr_host, arrHost, originalFileName, currentFileName, headers, arrApp, id; + var _a, _b, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + lib = require('../../../../../methods/lib')(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign + args.inputs = lib.loadDefaultValues(args.inputs, details); + refreshed = false; + arr = String(args.inputs.arr); + arr_host = String(args.inputs.arr_host).trim(); + arrHost = arr_host.endsWith('/') ? arr_host.slice(0, -1) : arr_host; + originalFileName = (_b = (_a = args.originalLibraryFile) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : ''; + currentFileName = (_d = (_c = args.inputFileObj) === null || _c === void 0 ? void 0 : _c._id) !== null && _d !== void 0 ? _d : ''; + headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': String(args.inputs.arr_api_key), + Accept: 'application/json', + }; + arrApp = arr === 'radarr' + ? { + name: arr, + host: arrHost, + headers: headers, + content: 'Movie', + delegates: { + getIdFromParseResponse: function (parseResponse) { var _a, _b, _c; return Number((_c = (_b = (_a = parseResponse === null || parseResponse === void 0 ? void 0 : parseResponse.data) === null || _a === void 0 ? void 0 : _a.movie) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : -1); }, + buildRefreshResquestData: function (id) { return JSON.stringify({ name: 'RefreshMovie', movieIds: [id] }); }, + }, + } + : { + name: arr, + host: arrHost, + headers: headers, + content: 'Serie', + delegates: { + getIdFromParseResponse: function (parseResponse) { var _a, _b, _c; return Number((_c = (_b = (_a = parseResponse === null || parseResponse === void 0 ? void 0 : parseResponse.data) === null || _a === void 0 ? void 0 : _a.series) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : -1); }, + buildRefreshResquestData: function (id) { return JSON.stringify({ name: 'RefreshSeries', seriesId: id }); }, + }, + }; + args.jobLog('Going to force scan'); + args.jobLog("Refreshing ".concat(arrApp.name, "...")); + return [4 /*yield*/, getId(args, arrApp, originalFileName)]; + case 1: + id = _e.sent(); + if (!(id === -1 && currentFileName !== originalFileName)) return [3 /*break*/, 3]; + return [4 /*yield*/, getId(args, arrApp, currentFileName)]; + case 2: + id = _e.sent(); + _e.label = 3; + case 3: + if (!(id !== -1)) return [3 /*break*/, 5]; + // Using command endpoint to queue a refresh task + return [4 /*yield*/, args.deps.axios({ + method: 'post', + url: "".concat(arrApp.host, "/api/v3/command"), + headers: headers, + data: arrApp.delegates.buildRefreshResquestData(id), + })]; + case 4: + // Using command endpoint to queue a refresh task + _e.sent(); + refreshed = true; + args.jobLog("\u2714 ".concat(arrApp.content, " '").concat(id, "' refreshed in ").concat(arrApp.name, ".")); + _e.label = 5; + case 5: return [2 /*return*/, { + outputFileObj: args.inputFileObj, + outputNumber: refreshed ? 1 : 2, + variables: args.variables, + }]; + } + }); +}); }; +exports.plugin = plugin; diff --git a/FlowPlugins/FlowHelpers/1.0.0/fileMoveOrCopy.js b/FlowPlugins/FlowHelpers/1.0.0/fileMoveOrCopy.js index c6f586691..8dc2dbc57 100644 --- a/FlowPlugins/FlowHelpers/1.0.0/fileMoveOrCopy.js +++ b/FlowPlugins/FlowHelpers/1.0.0/fileMoveOrCopy.js @@ -69,260 +69,248 @@ var compareOldNew = function (_a) { + " cache file of size ".concat(sourceFileSize)); } }; -var tryMove = function (_a) { - var sourcePath = _a.sourcePath, destinationPath = _a.destinationPath, sourceFileSize = _a.sourceFileSize, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var error, err_2, destinationSize; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - args.jobLog("Attempting move from ".concat(sourcePath, " to ").concat(destinationPath, ", method 1")); - error = false; - _b.label = 1; - case 1: - _b.trys.push([1, 3, , 4]); - return [4 /*yield*/, fs_1.promises.rename(sourcePath, destinationPath)]; - case 2: - _b.sent(); - return [3 /*break*/, 4]; - case 3: - err_2 = _b.sent(); - error = true; - args.jobLog("File move error: ".concat(JSON.stringify(err_2))); - return [3 /*break*/, 4]; - case 4: return [4 /*yield*/, getSizeBytes(destinationPath)]; - case 5: - destinationSize = _b.sent(); - compareOldNew({ - sourceFileSize: sourceFileSize, - destinationSize: destinationSize, - args: args, - }); - if (error || destinationSize !== sourceFileSize) { - return [2 /*return*/, false]; - } - return [2 /*return*/, true]; - } - }); +var tryMove = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var error, err_2, destinationSize; + var sourcePath = _b.sourcePath, destinationPath = _b.destinationPath, sourceFileSize = _b.sourceFileSize, args = _b.args; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + args.jobLog("Attempting move from ".concat(sourcePath, " to ").concat(destinationPath, ", method 1")); + error = false; + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, fs_1.promises.rename(sourcePath, destinationPath)]; + case 2: + _c.sent(); + return [3 /*break*/, 4]; + case 3: + err_2 = _c.sent(); + error = true; + args.jobLog("File move error: ".concat(JSON.stringify(err_2))); + return [3 /*break*/, 4]; + case 4: return [4 /*yield*/, getSizeBytes(destinationPath)]; + case 5: + destinationSize = _c.sent(); + compareOldNew({ + sourceFileSize: sourceFileSize, + destinationSize: destinationSize, + args: args, + }); + if (error || destinationSize !== sourceFileSize) { + return [2 /*return*/, false]; + } + return [2 /*return*/, true]; + } }); -}; +}); }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -var tryMvdir = function (_a) { - var sourcePath = _a.sourcePath, destinationPath = _a.destinationPath, sourceFileSize = _a.sourceFileSize, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var error, destinationSize; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - args.jobLog("Attempting move from ".concat(sourcePath, " to ").concat(destinationPath, ", method 2")); - error = false; - return [4 /*yield*/, new Promise(function (resolve) { - // fs-extra and move-file don't work when destination is on windows root of drive - // mvdir will try to move else fall back to copy/unlink - // potential bug on unraid - args.deps.mvdir(sourcePath, destinationPath, { overwrite: true }) - .then(function () { - resolve(true); - }).catch(function (err) { - error = true; - args.jobLog("File move error: ".concat(err)); - resolve(err); - }); - })]; - case 1: - _b.sent(); - return [4 /*yield*/, getSizeBytes(destinationPath)]; - case 2: - destinationSize = _b.sent(); - compareOldNew({ - sourceFileSize: sourceFileSize, - destinationSize: destinationSize, - args: args, - }); - if (error || destinationSize !== sourceFileSize) { - return [2 /*return*/, false]; - } - return [2 /*return*/, true]; - } - }); +var tryMvdir = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var error, destinationSize; + var sourcePath = _b.sourcePath, destinationPath = _b.destinationPath, sourceFileSize = _b.sourceFileSize, args = _b.args; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + args.jobLog("Attempting move from ".concat(sourcePath, " to ").concat(destinationPath, ", method 2")); + error = false; + return [4 /*yield*/, new Promise(function (resolve) { + // fs-extra and move-file don't work when destination is on windows root of drive + // mvdir will try to move else fall back to copy/unlink + // potential bug on unraid + args.deps.mvdir(sourcePath, destinationPath, { overwrite: true }) + .then(function () { + resolve(true); + }).catch(function (err) { + error = true; + args.jobLog("File move error: ".concat(err)); + resolve(err); + }); + })]; + case 1: + _c.sent(); + return [4 /*yield*/, getSizeBytes(destinationPath)]; + case 2: + destinationSize = _c.sent(); + compareOldNew({ + sourceFileSize: sourceFileSize, + destinationSize: destinationSize, + args: args, + }); + if (error || destinationSize !== sourceFileSize) { + return [2 /*return*/, false]; + } + return [2 /*return*/, true]; + } }); -}; +}); }; // Keep in e.g. https://github.com/HaveAGitGat/Tdarr/issues/858 -var tyNcp = function (_a) { - var sourcePath = _a.sourcePath, destinationPath = _a.destinationPath, sourceFileSize = _a.sourceFileSize, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var error_1, destinationSize; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - if (!args.deps.ncp) return [3 /*break*/, 3]; - args.jobLog("Attempting copy from ".concat(sourcePath, " to ").concat(destinationPath, " , method 1")); - error_1 = false; - return [4 /*yield*/, new Promise(function (resolve) { - args.deps.ncp(sourcePath, destinationPath, function (err) { - if (err) { - error_1 = true; - args.jobLog("File copy error: ".concat(err)); - resolve(err); - } - else { - resolve(true); - } - }); - })]; - case 1: - _b.sent(); - return [4 /*yield*/, getSizeBytes(destinationPath)]; - case 2: - destinationSize = _b.sent(); - compareOldNew({ - sourceFileSize: sourceFileSize, - destinationSize: destinationSize, - args: args, - }); - if (error_1 || destinationSize !== sourceFileSize) { - return [2 /*return*/, false]; - } - return [2 /*return*/, true]; - case 3: return [2 /*return*/, false]; - } - }); +var tyNcp = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var error_1, destinationSize; + var sourcePath = _b.sourcePath, destinationPath = _b.destinationPath, sourceFileSize = _b.sourceFileSize, args = _b.args; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!args.deps.ncp) return [3 /*break*/, 3]; + args.jobLog("Attempting copy from ".concat(sourcePath, " to ").concat(destinationPath, " , method 1")); + error_1 = false; + return [4 /*yield*/, new Promise(function (resolve) { + args.deps.ncp(sourcePath, destinationPath, function (err) { + if (err) { + error_1 = true; + args.jobLog("File copy error: ".concat(err)); + resolve(err); + } + else { + resolve(true); + } + }); + })]; + case 1: + _c.sent(); + return [4 /*yield*/, getSizeBytes(destinationPath)]; + case 2: + destinationSize = _c.sent(); + compareOldNew({ + sourceFileSize: sourceFileSize, + destinationSize: destinationSize, + args: args, + }); + if (error_1 || destinationSize !== sourceFileSize) { + return [2 /*return*/, false]; + } + return [2 /*return*/, true]; + case 3: return [2 /*return*/, false]; + } }); -}; -var tryNormalCopy = function (_a) { - var sourcePath = _a.sourcePath, destinationPath = _a.destinationPath, sourceFileSize = _a.sourceFileSize, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var error, err_3, destinationSize; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - args.jobLog("Attempting copy from ".concat(sourcePath, " to ").concat(destinationPath, " , method 2")); - error = false; - _b.label = 1; - case 1: - _b.trys.push([1, 3, , 4]); - return [4 /*yield*/, fs_1.promises.copyFile(sourcePath, destinationPath)]; - case 2: - _b.sent(); - return [3 /*break*/, 4]; - case 3: - err_3 = _b.sent(); - error = true; - args.jobLog("File copy error: ".concat(JSON.stringify(err_3))); - return [3 /*break*/, 4]; - case 4: return [4 /*yield*/, getSizeBytes(destinationPath)]; - case 5: - destinationSize = _b.sent(); - compareOldNew({ - sourceFileSize: sourceFileSize, - destinationSize: destinationSize, - args: args, - }); - if (error || destinationSize !== sourceFileSize) { - return [2 /*return*/, false]; - } - return [2 /*return*/, true]; - } - }); +}); }; +var tryNormalCopy = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var error, err_3, destinationSize; + var sourcePath = _b.sourcePath, destinationPath = _b.destinationPath, sourceFileSize = _b.sourceFileSize, args = _b.args; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + args.jobLog("Attempting copy from ".concat(sourcePath, " to ").concat(destinationPath, " , method 2")); + error = false; + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, fs_1.promises.copyFile(sourcePath, destinationPath)]; + case 2: + _c.sent(); + return [3 /*break*/, 4]; + case 3: + err_3 = _c.sent(); + error = true; + args.jobLog("File copy error: ".concat(JSON.stringify(err_3))); + return [3 /*break*/, 4]; + case 4: return [4 /*yield*/, getSizeBytes(destinationPath)]; + case 5: + destinationSize = _c.sent(); + compareOldNew({ + sourceFileSize: sourceFileSize, + destinationSize: destinationSize, + args: args, + }); + if (error || destinationSize !== sourceFileSize) { + return [2 /*return*/, false]; + } + return [2 /*return*/, true]; + } }); -}; -var cleanSourceFile = function (_a) { - var args = _a.args, sourcePath = _a.sourcePath; - return __awaiter(void 0, void 0, void 0, function () { - var err_4; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - _b.trys.push([0, 2, , 3]); - args.jobLog("Deleting source file ".concat(sourcePath)); - return [4 /*yield*/, fs_1.promises.unlink(sourcePath)]; - case 1: - _b.sent(); - return [3 /*break*/, 3]; - case 2: - err_4 = _b.sent(); - args.jobLog("Failed to delete source file ".concat(sourcePath, ": ").concat(JSON.stringify(err_4))); - return [3 /*break*/, 3]; - case 3: return [2 /*return*/]; - } - }); +}); }; +var cleanSourceFile = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var err_4; + var args = _b.args, sourcePath = _b.sourcePath; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + _c.trys.push([0, 2, , 3]); + args.jobLog("Deleting source file ".concat(sourcePath)); + return [4 /*yield*/, fs_1.promises.unlink(sourcePath)]; + case 1: + _c.sent(); + return [3 /*break*/, 3]; + case 2: + err_4 = _c.sent(); + args.jobLog("Failed to delete source file ".concat(sourcePath, ": ").concat(JSON.stringify(err_4))); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } }); -}; -var fileMoveOrCopy = function (_a) { - var operation = _a.operation, sourcePath = _a.sourcePath, destinationPath = _a.destinationPath, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var sourceFileSize, moved, ncpd, copied; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - args.jobLog('Calculating cache file size in bytes'); - return [4 /*yield*/, getSizeBytes(sourcePath)]; - case 1: - sourceFileSize = _b.sent(); - args.jobLog("".concat(sourceFileSize)); - if (!(operation === 'move')) return [3 /*break*/, 3]; - return [4 /*yield*/, tryMove({ - sourcePath: sourcePath, - destinationPath: destinationPath, - args: args, - sourceFileSize: sourceFileSize, - })]; - case 2: - moved = _b.sent(); - if (moved) { - return [2 /*return*/, true]; - } - // disable: https://github.com/HaveAGitGat/Tdarr/issues/885 - // const mvdird = await tryMvdir({ - // sourcePath, - // destinationPath, - // args, - // sourceFileSize, - // }); - // if (mvdird) { - // return true; - // } - args.jobLog('Failed to move file, trying copy'); - _b.label = 3; - case 3: return [4 /*yield*/, tyNcp({ +}); }; +var fileMoveOrCopy = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var sourceFileSize, moved, ncpd, copied; + var operation = _b.operation, sourcePath = _b.sourcePath, destinationPath = _b.destinationPath, args = _b.args; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + args.jobLog('Calculating cache file size in bytes'); + return [4 /*yield*/, getSizeBytes(sourcePath)]; + case 1: + sourceFileSize = _c.sent(); + args.jobLog("".concat(sourceFileSize)); + if (!(operation === 'move')) return [3 /*break*/, 3]; + return [4 /*yield*/, tryMove({ sourcePath: sourcePath, destinationPath: destinationPath, args: args, sourceFileSize: sourceFileSize, })]; - case 4: - ncpd = _b.sent(); - if (!ncpd) return [3 /*break*/, 7]; - if (!(operation === 'move')) return [3 /*break*/, 6]; - return [4 /*yield*/, cleanSourceFile({ - args: args, - sourcePath: sourcePath, - })]; - case 5: - _b.sent(); - _b.label = 6; - case 6: return [2 /*return*/, true]; - case 7: return [4 /*yield*/, tryNormalCopy({ + case 2: + moved = _c.sent(); + if (moved) { + return [2 /*return*/, true]; + } + // disable: https://github.com/HaveAGitGat/Tdarr/issues/885 + // const mvdird = await tryMvdir({ + // sourcePath, + // destinationPath, + // args, + // sourceFileSize, + // }); + // if (mvdird) { + // return true; + // } + args.jobLog('Failed to move file, trying copy'); + _c.label = 3; + case 3: return [4 /*yield*/, tyNcp({ + sourcePath: sourcePath, + destinationPath: destinationPath, + args: args, + sourceFileSize: sourceFileSize, + })]; + case 4: + ncpd = _c.sent(); + if (!ncpd) return [3 /*break*/, 7]; + if (!(operation === 'move')) return [3 /*break*/, 6]; + return [4 /*yield*/, cleanSourceFile({ + args: args, sourcePath: sourcePath, - destinationPath: destinationPath, + })]; + case 5: + _c.sent(); + _c.label = 6; + case 6: return [2 /*return*/, true]; + case 7: return [4 /*yield*/, tryNormalCopy({ + sourcePath: sourcePath, + destinationPath: destinationPath, + args: args, + sourceFileSize: sourceFileSize, + })]; + case 8: + copied = _c.sent(); + if (!copied) return [3 /*break*/, 11]; + if (!(operation === 'move')) return [3 /*break*/, 10]; + return [4 /*yield*/, cleanSourceFile({ args: args, - sourceFileSize: sourceFileSize, + sourcePath: sourcePath, })]; - case 8: - copied = _b.sent(); - if (!copied) return [3 /*break*/, 11]; - if (!(operation === 'move')) return [3 /*break*/, 10]; - return [4 /*yield*/, cleanSourceFile({ - args: args, - sourcePath: sourcePath, - })]; - case 9: - _b.sent(); - _b.label = 10; - case 10: return [2 /*return*/, true]; - case 11: throw new Error("Failed to ".concat(operation, " file")); - } - }); + case 9: + _c.sent(); + _c.label = 10; + case 10: return [2 /*return*/, true]; + case 11: throw new Error("Failed to ".concat(operation, " file")); + } }); -}; +}); }; exports.default = fileMoveOrCopy; diff --git a/FlowPlugins/FlowHelpers/1.0.0/fileUtils.js b/FlowPlugins/FlowHelpers/1.0.0/fileUtils.js index 3f777d334..1e3604571 100644 --- a/FlowPlugins/FlowHelpers/1.0.0/fileUtils.js +++ b/FlowPlugins/FlowHelpers/1.0.0/fileUtils.js @@ -80,80 +80,78 @@ var getFileSize = function (file) { return __awaiter(void 0, void 0, void 0, fun }); }); }; exports.getFileSize = getFileSize; -var moveFileAndValidate = function (_a) { - var inputPath = _a.inputPath, outputPath = _a.outputPath, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var inputSize, res1, outputSize, err_1, res2, errMessage; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: return [4 /*yield*/, (0, exports.getFileSize)(inputPath)]; - case 1: - inputSize = _b.sent(); - args.jobLog("Attempt 1: Moving file from ".concat(inputPath, " to ").concat(outputPath)); - return [4 /*yield*/, new Promise(function (resolve) { - args.deps.gracefulfs.rename(inputPath, outputPath, function (err) { - if (err) { - args.jobLog("Failed to move file from ".concat(inputPath, " to ").concat(outputPath)); - args.jobLog(JSON.stringify(err)); - resolve(false); - } - else { - resolve(true); - } - }); - })]; - case 2: - res1 = _b.sent(); - outputSize = 0; - _b.label = 3; - case 3: - _b.trys.push([3, 5, , 6]); - return [4 /*yield*/, (0, exports.getFileSize)(outputPath)]; - case 4: - outputSize = _b.sent(); - return [3 /*break*/, 6]; - case 5: - err_1 = _b.sent(); - args.jobLog(JSON.stringify(err_1)); - return [3 /*break*/, 6]; - case 6: - if (!(!res1 || inputSize !== outputSize)) return [3 /*break*/, 9]; - if (inputSize !== outputSize) { - args.jobLog("File sizes do not match, input: ".concat(inputSize, " ") - + "does not equal output: ".concat(outputSize)); - } - args.jobLog("Attempt 1 failed: Moving file from ".concat(inputPath, " to ").concat(outputPath)); - args.jobLog("Attempt 2: Moving file from ".concat(inputPath, " to ").concat(outputPath)); - return [4 /*yield*/, new Promise(function (resolve) { - args.deps.mvdir(inputPath, outputPath, { overwrite: true }) - .then(function () { - resolve(true); - }).catch(function (err) { +var moveFileAndValidate = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var inputSize, res1, outputSize, err_1, res2, errMessage; + var inputPath = _b.inputPath, outputPath = _b.outputPath, args = _b.args; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, (0, exports.getFileSize)(inputPath)]; + case 1: + inputSize = _c.sent(); + args.jobLog("Attempt 1: Moving file from ".concat(inputPath, " to ").concat(outputPath)); + return [4 /*yield*/, new Promise(function (resolve) { + args.deps.gracefulfs.rename(inputPath, outputPath, function (err) { + if (err) { args.jobLog("Failed to move file from ".concat(inputPath, " to ").concat(outputPath)); args.jobLog(JSON.stringify(err)); resolve(false); - }); - })]; - case 7: - res2 = _b.sent(); - return [4 /*yield*/, (0, exports.getFileSize)(outputPath)]; - case 8: - outputSize = _b.sent(); - if (!res2 || inputSize !== outputSize) { - if (inputSize !== outputSize) { - args.jobLog("File sizes do not match, input: ".concat(inputSize, " ") - + "does not equal output: ".concat(outputSize)); - } - errMessage = "Failed to move file from ".concat(inputPath, " to ").concat(outputPath, ", check errors above"); - args.jobLog(errMessage); - throw new Error(errMessage); + } + else { + resolve(true); + } + }); + })]; + case 2: + res1 = _c.sent(); + outputSize = 0; + _c.label = 3; + case 3: + _c.trys.push([3, 5, , 6]); + return [4 /*yield*/, (0, exports.getFileSize)(outputPath)]; + case 4: + outputSize = _c.sent(); + return [3 /*break*/, 6]; + case 5: + err_1 = _c.sent(); + args.jobLog(JSON.stringify(err_1)); + return [3 /*break*/, 6]; + case 6: + if (!(!res1 || inputSize !== outputSize)) return [3 /*break*/, 9]; + if (inputSize !== outputSize) { + args.jobLog("File sizes do not match, input: ".concat(inputSize, " ") + + "does not equal output: ".concat(outputSize)); + } + args.jobLog("Attempt 1 failed: Moving file from ".concat(inputPath, " to ").concat(outputPath)); + args.jobLog("Attempt 2: Moving file from ".concat(inputPath, " to ").concat(outputPath)); + return [4 /*yield*/, new Promise(function (resolve) { + args.deps.mvdir(inputPath, outputPath, { overwrite: true }) + .then(function () { + resolve(true); + }).catch(function (err) { + args.jobLog("Failed to move file from ".concat(inputPath, " to ").concat(outputPath)); + args.jobLog(JSON.stringify(err)); + resolve(false); + }); + })]; + case 7: + res2 = _c.sent(); + return [4 /*yield*/, (0, exports.getFileSize)(outputPath)]; + case 8: + outputSize = _c.sent(); + if (!res2 || inputSize !== outputSize) { + if (inputSize !== outputSize) { + args.jobLog("File sizes do not match, input: ".concat(inputSize, " ") + + "does not equal output: ".concat(outputSize)); } - _b.label = 9; - case 9: return [2 /*return*/]; - } - }); + errMessage = "Failed to move file from ".concat(inputPath, " to ").concat(outputPath, ", check errors above"); + args.jobLog(errMessage); + throw new Error(errMessage); + } + _c.label = 9; + case 9: return [2 /*return*/]; + } }); -}; +}); }; exports.moveFileAndValidate = moveFileAndValidate; var getPluginWorkDir = function (args) { var pluginWorkDir = "".concat(args.workDir, "/").concat(new Date().getTime()); diff --git a/FlowPlugins/FlowHelpers/1.0.0/hardwareUtils.js b/FlowPlugins/FlowHelpers/1.0.0/hardwareUtils.js index da276ebb5..1b188ef40 100644 --- a/FlowPlugins/FlowHelpers/1.0.0/hardwareUtils.js +++ b/FlowPlugins/FlowHelpers/1.0.0/hardwareUtils.js @@ -61,80 +61,78 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); exports.getEncoder = exports.getBestNvencDevice = exports.hasEncoder = void 0; var os_1 = __importDefault(require("os")); -var hasEncoder = function (_a) { - var ffmpegPath = _a.ffmpegPath, encoder = _a.encoder, inputArgs = _a.inputArgs, outputArgs = _a.outputArgs, filter = _a.filter, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var spawn, isEnabled, commandArr_1, err_1; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - spawn = require('child_process').spawn; - isEnabled = false; - _b.label = 1; - case 1: - _b.trys.push([1, 3, , 4]); - commandArr_1 = __spreadArray(__spreadArray(__spreadArray(__spreadArray(__spreadArray(__spreadArray([], inputArgs, true), [ - '-f', - 'lavfi', - '-i', - 'color=c=black:s=256x256:d=1:r=30' - ], false), (filter ? filter.split(' ') : []), true), [ - '-c:v', - encoder - ], false), outputArgs, true), [ - '-f', - 'null', - '/dev/null', - ], false); - args.jobLog("Checking for encoder ".concat(encoder, " with command:")); - args.jobLog("".concat(ffmpegPath, " ").concat(commandArr_1.join(' '))); - return [4 /*yield*/, new Promise(function (resolve) { - var error = function () { - resolve(false); - }; - var stderr = ''; - try { - var thread = spawn(ffmpegPath, commandArr_1); - thread.on('error', function () { - // catches execution error (bad file) - error(); - }); - thread.stdout.on('data', function (data) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - stderr += data; - }); - thread.stderr.on('data', function (data) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - stderr += data; - }); - thread.on('close', function (code) { - if (code !== 0) { - error(); - } - else { - resolve(true); - } - }); - } - catch (err) { - // catches execution error (no file) +var hasEncoder = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var spawn, isEnabled, commandArr_1, err_1; + var ffmpegPath = _b.ffmpegPath, encoder = _b.encoder, inputArgs = _b.inputArgs, outputArgs = _b.outputArgs, filter = _b.filter, args = _b.args; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + spawn = require('child_process').spawn; + isEnabled = false; + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + commandArr_1 = __spreadArray(__spreadArray(__spreadArray(__spreadArray(__spreadArray(__spreadArray([], inputArgs, true), [ + '-f', + 'lavfi', + '-i', + 'color=c=black:s=256x256:d=1:r=30' + ], false), (filter ? filter.split(' ') : []), true), [ + '-c:v', + encoder + ], false), outputArgs, true), [ + '-f', + 'null', + '/dev/null', + ], false); + args.jobLog("Checking for encoder ".concat(encoder, " with command:")); + args.jobLog("".concat(ffmpegPath, " ").concat(commandArr_1.join(' '))); + return [4 /*yield*/, new Promise(function (resolve) { + var error = function () { + resolve(false); + }; + var stderr = ''; + try { + var thread = spawn(ffmpegPath, commandArr_1); + thread.on('error', function () { + // catches execution error (bad file) error(); - } - })]; - case 2: - isEnabled = _b.sent(); - args.jobLog("Encoder ".concat(encoder, " is ").concat(isEnabled ? 'enabled' : 'disabled')); - return [3 /*break*/, 4]; - case 3: - err_1 = _b.sent(); - // eslint-disable-next-line no-console - console.log(err_1); - return [3 /*break*/, 4]; - case 4: return [2 /*return*/, isEnabled]; - } - }); + }); + thread.stdout.on('data', function (data) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + stderr += data; + }); + thread.stderr.on('data', function (data) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + stderr += data; + }); + thread.on('close', function (code) { + if (code !== 0) { + error(); + } + else { + resolve(true); + } + }); + } + catch (err) { + // catches execution error (no file) + error(); + } + })]; + case 2: + isEnabled = _c.sent(); + args.jobLog("Encoder ".concat(encoder, " is ").concat(isEnabled ? 'enabled' : 'disabled')); + return [3 /*break*/, 4]; + case 3: + err_1 = _c.sent(); + // eslint-disable-next-line no-console + console.log(err_1); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/, isEnabled]; + } }); -}; +}); }; exports.hasEncoder = hasEncoder; // credit to UNCode101 for this var getBestNvencDevice = function (_a) { @@ -207,236 +205,234 @@ var encoderFilter = function (encoder, targetCodec) { } return false; }; -var getEncoder = function (_a) { - var targetCodec = _a.targetCodec, hardwareEncoding = _a.hardwareEncoding, hardwareType = _a.hardwareType, args = _a.args; - return __awaiter(void 0, void 0, void 0, function () { - var supportedGpuEncoders, gpuEncoders, filteredGpuEncoders, idx, _i, filteredGpuEncoders_1, gpuEncoder, _b, enabledDevices, res; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - supportedGpuEncoders = ['hevc', 'h264', 'av1']; - if (!(args.workerType - && args.workerType.includes('gpu') - && hardwareEncoding && (supportedGpuEncoders.includes(targetCodec)))) return [3 /*break*/, 5]; - gpuEncoders = [ - { - encoder: 'hevc_nvenc', - enabled: false, - inputArgs: [ - '-hwaccel', - 'cuda', - ], - outputArgs: [], - filter: '', - }, - { - encoder: 'hevc_amf', - enabled: false, - inputArgs: [], - outputArgs: [], - filter: '', - }, - { - encoder: 'hevc_qsv', - enabled: false, - inputArgs: [ - '-hwaccel', - 'qsv', - ], - outputArgs: __spreadArray([], (os_1.default.platform() === 'win32' ? ['-load_plugin', 'hevc_hw'] : []), true), - filter: '', - }, - { - encoder: 'hevc_vaapi', - inputArgs: [ - '-hwaccel', - 'vaapi', - '-hwaccel_device', - '/dev/dri/renderD128', - '-hwaccel_output_format', - 'vaapi', - ], - outputArgs: [], - enabled: false, - filter: '-vf format=nv12,hwupload', - }, - { - encoder: 'hevc_videotoolbox', - enabled: false, - inputArgs: [ - '-hwaccel', - 'videotoolbox', - ], - outputArgs: [], - filter: '', - }, - // h264 - { - encoder: 'h264_nvenc', - enabled: false, - inputArgs: [ - '-hwaccel', - 'cuda', - ], - outputArgs: [], - filter: '', - }, - { - encoder: 'h264_amf', - enabled: false, - inputArgs: [], - outputArgs: [], - filter: '', - }, - { - encoder: 'h264_qsv', - enabled: false, - inputArgs: [ - '-hwaccel', - 'qsv', - ], - outputArgs: [], - filter: '', - }, - { - encoder: 'h264_videotoolbox', - enabled: false, - inputArgs: [ - '-hwaccel', - 'videotoolbox', - ], - outputArgs: [], - filter: '', - }, - // av1 - { - encoder: 'av1_nvenc', - enabled: false, - inputArgs: [], - outputArgs: [], - filter: '', - }, - { - encoder: 'av1_amf', - enabled: false, - inputArgs: [], - outputArgs: [], - filter: '', - }, - { - encoder: 'av1_qsv', - enabled: false, +var getEncoder = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { + var supportedGpuEncoders, gpuEncoders, filteredGpuEncoders, idx, _i, filteredGpuEncoders_1, gpuEncoder, _c, enabledDevices, res; + var targetCodec = _b.targetCodec, hardwareEncoding = _b.hardwareEncoding, hardwareType = _b.hardwareType, args = _b.args; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + supportedGpuEncoders = ['hevc', 'h264', 'av1']; + if (!(args.workerType + && args.workerType.includes('gpu') + && hardwareEncoding && (supportedGpuEncoders.includes(targetCodec)))) return [3 /*break*/, 5]; + gpuEncoders = [ + { + encoder: 'hevc_nvenc', + enabled: false, + inputArgs: [ + '-hwaccel', + 'cuda', + ], + outputArgs: [], + filter: '', + }, + { + encoder: 'hevc_amf', + enabled: false, + inputArgs: [], + outputArgs: [], + filter: '', + }, + { + encoder: 'hevc_qsv', + enabled: false, + inputArgs: [ + '-hwaccel', + 'qsv', + ], + outputArgs: __spreadArray([], (os_1.default.platform() === 'win32' ? ['-load_plugin', 'hevc_hw'] : []), true), + filter: '', + }, + { + encoder: 'hevc_vaapi', + inputArgs: [ + '-hwaccel', + 'vaapi', + '-hwaccel_device', + '/dev/dri/renderD128', + '-hwaccel_output_format', + 'vaapi', + ], + outputArgs: [], + enabled: false, + filter: '-vf format=nv12,hwupload', + }, + { + encoder: 'hevc_videotoolbox', + enabled: false, + inputArgs: [ + '-hwaccel', + 'videotoolbox', + ], + outputArgs: [], + filter: '', + }, + // h264 + { + encoder: 'h264_nvenc', + enabled: false, + inputArgs: [ + '-hwaccel', + 'cuda', + ], + outputArgs: [], + filter: '', + }, + { + encoder: 'h264_amf', + enabled: false, + inputArgs: [], + outputArgs: [], + filter: '', + }, + { + encoder: 'h264_qsv', + enabled: false, + inputArgs: [ + '-hwaccel', + 'qsv', + ], + outputArgs: [], + filter: '', + }, + { + encoder: 'h264_videotoolbox', + enabled: false, + inputArgs: [ + '-hwaccel', + 'videotoolbox', + ], + outputArgs: [], + filter: '', + }, + // av1 + { + encoder: 'av1_nvenc', + enabled: false, + inputArgs: [], + outputArgs: [], + filter: '', + }, + { + encoder: 'av1_amf', + enabled: false, + inputArgs: [], + outputArgs: [], + filter: '', + }, + { + encoder: 'av1_qsv', + enabled: false, + inputArgs: [], + outputArgs: [], + filter: '', + }, + { + encoder: 'av1_vaapi', + enabled: false, + inputArgs: [], + outputArgs: [], + filter: '', + }, + ]; + filteredGpuEncoders = gpuEncoders.filter(function (device) { return encoderFilter(device.encoder, targetCodec); }); + if (hardwareEncoding && hardwareType !== 'auto') { + idx = filteredGpuEncoders.findIndex(function (device) { return device.encoder.includes(hardwareType); }); + if (idx === -1) { + throw new Error("Could not find encoder ".concat(targetCodec, " for hardware ").concat(hardwareType)); + } + return [2 /*return*/, __assign(__assign({}, filteredGpuEncoders[idx]), { isGpu: true, enabledDevices: [] })]; + } + args.jobLog(JSON.stringify({ filteredGpuEncoders: filteredGpuEncoders })); + _i = 0, filteredGpuEncoders_1 = filteredGpuEncoders; + _d.label = 1; + case 1: + if (!(_i < filteredGpuEncoders_1.length)) return [3 /*break*/, 4]; + gpuEncoder = filteredGpuEncoders_1[_i]; + // eslint-disable-next-line no-await-in-loop + _c = gpuEncoder; + return [4 /*yield*/, (0, exports.hasEncoder)({ + ffmpegPath: args.ffmpegPath, + encoder: gpuEncoder.encoder, + inputArgs: gpuEncoder.inputArgs, + outputArgs: gpuEncoder.outputArgs, + filter: gpuEncoder.filter, + args: args, + })]; + case 2: + // eslint-disable-next-line no-await-in-loop + _c.enabled = _d.sent(); + _d.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: + enabledDevices = filteredGpuEncoders.filter(function (device) { return device.enabled === true; }); + args.jobLog(JSON.stringify({ enabledDevices: enabledDevices })); + if (enabledDevices.length > 0) { + if (enabledDevices[0].encoder.includes('nvenc')) { + res = (0, exports.getBestNvencDevice)({ + args: args, + nvencDevice: enabledDevices[0], + }); + return [2 /*return*/, __assign(__assign({}, res), { isGpu: true, enabledDevices: enabledDevices })]; + } + return [2 /*return*/, { + encoder: enabledDevices[0].encoder, + inputArgs: enabledDevices[0].inputArgs, + outputArgs: enabledDevices[0].outputArgs, + isGpu: true, + enabledDevices: enabledDevices, + }]; + } + return [3 /*break*/, 6]; + case 5: + if (!hardwareEncoding) { + args.jobLog('Hardware encoding is disabled in plugin input options'); + } + if (!args.workerType || !args.workerType.includes('gpu')) { + args.jobLog('Worker type is not GPU'); + } + if (!supportedGpuEncoders.includes(targetCodec)) { + args.jobLog("Target codec ".concat(targetCodec, " is not supported for GPU encoding")); + } + _d.label = 6; + case 6: + if (targetCodec === 'hevc') { + return [2 /*return*/, { + encoder: 'libx265', inputArgs: [], outputArgs: [], - filter: '', - }, - { - encoder: 'av1_vaapi', - enabled: false, + isGpu: false, + enabledDevices: [], + }]; + } + if (targetCodec === 'h264') { + return [2 /*return*/, { + encoder: 'libx264', inputArgs: [], outputArgs: [], - filter: '', - }, - ]; - filteredGpuEncoders = gpuEncoders.filter(function (device) { return encoderFilter(device.encoder, targetCodec); }); - if (hardwareEncoding && hardwareType !== 'auto') { - idx = filteredGpuEncoders.findIndex(function (device) { return device.encoder.includes(hardwareType); }); - if (idx === -1) { - throw new Error("Could not find encoder ".concat(targetCodec, " for hardware ").concat(hardwareType)); - } - return [2 /*return*/, __assign(__assign({}, filteredGpuEncoders[idx]), { isGpu: true, enabledDevices: [] })]; - } - args.jobLog(JSON.stringify({ filteredGpuEncoders: filteredGpuEncoders })); - _i = 0, filteredGpuEncoders_1 = filteredGpuEncoders; - _c.label = 1; - case 1: - if (!(_i < filteredGpuEncoders_1.length)) return [3 /*break*/, 4]; - gpuEncoder = filteredGpuEncoders_1[_i]; - // eslint-disable-next-line no-await-in-loop - _b = gpuEncoder; - return [4 /*yield*/, (0, exports.hasEncoder)({ - ffmpegPath: args.ffmpegPath, - encoder: gpuEncoder.encoder, - inputArgs: gpuEncoder.inputArgs, - outputArgs: gpuEncoder.outputArgs, - filter: gpuEncoder.filter, - args: args, - })]; - case 2: - // eslint-disable-next-line no-await-in-loop - _b.enabled = _c.sent(); - _c.label = 3; - case 3: - _i++; - return [3 /*break*/, 1]; - case 4: - enabledDevices = filteredGpuEncoders.filter(function (device) { return device.enabled === true; }); - args.jobLog(JSON.stringify({ enabledDevices: enabledDevices })); - if (enabledDevices.length > 0) { - if (enabledDevices[0].encoder.includes('nvenc')) { - res = (0, exports.getBestNvencDevice)({ - args: args, - nvencDevice: enabledDevices[0], - }); - return [2 /*return*/, __assign(__assign({}, res), { isGpu: true, enabledDevices: enabledDevices })]; - } - return [2 /*return*/, { - encoder: enabledDevices[0].encoder, - inputArgs: enabledDevices[0].inputArgs, - outputArgs: enabledDevices[0].outputArgs, - isGpu: true, - enabledDevices: enabledDevices, - }]; - } - return [3 /*break*/, 6]; - case 5: - if (!hardwareEncoding) { - args.jobLog('Hardware encoding is disabled in plugin input options'); - } - if (!args.workerType || !args.workerType.includes('gpu')) { - args.jobLog('Worker type is not GPU'); - } - if (!supportedGpuEncoders.includes(targetCodec)) { - args.jobLog("Target codec ".concat(targetCodec, " is not supported for GPU encoding")); - } - _c.label = 6; - case 6: - if (targetCodec === 'hevc') { - return [2 /*return*/, { - encoder: 'libx265', - inputArgs: [], - outputArgs: [], - isGpu: false, - enabledDevices: [], - }]; - } - if (targetCodec === 'h264') { - return [2 /*return*/, { - encoder: 'libx264', - inputArgs: [], - outputArgs: [], - isGpu: false, - enabledDevices: [], - }]; - } - if (targetCodec === 'av1') { - return [2 /*return*/, { - encoder: 'libsvtav1', - inputArgs: [], - outputArgs: [], - isGpu: false, - enabledDevices: [], - }]; - } + isGpu: false, + enabledDevices: [], + }]; + } + if (targetCodec === 'av1') { return [2 /*return*/, { - encoder: targetCodec, + encoder: 'libsvtav1', inputArgs: [], outputArgs: [], isGpu: false, enabledDevices: [], }]; - } - }); + } + return [2 /*return*/, { + encoder: targetCodec, + inputArgs: [], + outputArgs: [], + isGpu: false, + enabledDevices: [], + }]; + } }); -}; +}); }; exports.getEncoder = getEncoder; diff --git a/FlowPluginsTs/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.ts b/FlowPluginsTs/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.ts index c3da552eb..178768552 100644 --- a/FlowPluginsTs/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.ts +++ b/FlowPluginsTs/CommunityFlowPlugins/ffmpegCommand/ffmpegCommandSetVideoBitrate/1.0.0/index.ts @@ -6,7 +6,7 @@ import { import { getFfType } from '../../../../FlowHelpers/1.0.0/fileUtils'; /* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */ -const details = () :IpluginDetails => ({ +const details = (): IpluginDetails => ({ name: 'Set Video Bitrate', description: 'Set Video Bitrate', style: { @@ -19,6 +19,67 @@ const details = () :IpluginDetails => ({ sidebarPosition: -1, icon: '', inputs: [ + { + label: 'Use % of Input Bitrate', + name: 'useInputBitrate', + type: 'boolean', + defaultValue: 'false', + inputUI: { + type: 'switch', + }, + tooltip: 'Specify whether to use a % of input bitrate as the output bitrate', + }, + + { + label: 'Target Bitrate %', + name: 'targetBitratePercent', + type: 'string', + defaultValue: '50', + inputUI: { + type: 'text', + displayConditions: { + logic: 'AND', + sets: [ + { + logic: 'AND', + inputs: [ + { + name: 'useInputBitrate', + value: 'true', + condition: '===', + }, + ], + }, + ], + }, + }, + tooltip: 'Specify the target bitrate as a % of the input bitrate', + }, + { + label: 'Fallback Bitrate', + name: 'fallbackBitrate', + type: 'string', + defaultValue: '4000', + inputUI: { + type: 'text', + displayConditions: { + logic: 'AND', + sets: [ + { + logic: 'AND', + inputs: [ + { + name: 'useInputBitrate', + value: 'true', + condition: '===', + }, + ], + }, + ], + }, + }, + tooltip: 'Specify fallback bitrate in kbps if input bitrate is not available', + }, { label: 'Bitrate', name: 'bitrate', @@ -26,6 +87,21 @@ const details = () :IpluginDetails => ({ defaultValue: '5000', inputUI: { type: 'text', + displayConditions: { + logic: 'AND', + sets: [ + { + logic: 'AND', + inputs: [ + { + name: 'useInputBitrate', + value: 'true', + condition: '!==', + }, + ], + }, + ], + }, }, tooltip: 'Specify bitrate in kbps', }, @@ -39,15 +115,40 @@ const details = () :IpluginDetails => ({ }); // eslint-disable-next-line @typescript-eslint/no-unused-vars -const plugin = (args:IpluginInputArgs):IpluginOutputArgs => { +const plugin = (args: IpluginInputArgs): IpluginOutputArgs => { const lib = require('../../../../../methods/lib')(); // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign args.inputs = lib.loadDefaultValues(args.inputs, details); + const { useInputBitrate } = args.inputs; + const targetBitratePercent = String(args.inputs.targetBitratePercent); + const fallbackBitrate = String(args.inputs.fallbackBitrate); + const bitrate = String(args.inputs.bitrate); + args.variables.ffmpegCommand.streams.forEach((stream) => { if (stream.codec_type === 'video') { const ffType = getFfType(stream.codec_type); - stream.outputArgs.push(`-b:${ffType}:{outputTypeIndex}`, `${String(args.inputs.bitrate)}k`); + if (useInputBitrate) { + args.jobLog('Attempting to use % of input bitrate as output bitrate'); + // check if input bitrate is available + const mediainfoIndex = stream.index + 1; + + let inputBitrate = args?.inputFileObj?.mediaInfo?.track?.[mediainfoIndex]?.BitRate; + if (inputBitrate) { + args.jobLog(`Found input bitrate: ${inputBitrate}`); + // @ts-expect-error type + inputBitrate = parseInt(inputBitrate, 10) / 1000; + const targetBitrate = (inputBitrate * (parseInt(targetBitratePercent, 10) / 100)); + args.jobLog(`Setting video bitrate as ${targetBitrate}k`); + stream.outputArgs.push(`-b:${ffType}:{outputTypeIndex}`, `${targetBitrate}k`); + } else { + args.jobLog(`Unable to find input bitrate, setting fallback bitrate as ${fallbackBitrate}k`); + stream.outputArgs.push(`-b:${ffType}:{outputTypeIndex}`, `${fallbackBitrate}k`); + } + } else { + args.jobLog(`Using fixed bitrate. Setting video bitrate as ${bitrate}k`); + stream.outputArgs.push(`-b:${ffType}:{outputTypeIndex}`, `${bitrate}k`); + } } }); diff --git a/FlowPluginsTs/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.ts b/FlowPluginsTs/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.ts index 7d6361f05..7265ac6b8 100644 --- a/FlowPluginsTs/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.ts +++ b/FlowPluginsTs/CommunityFlowPlugins/file/checkFileExtension/1.0.0/index.ts @@ -49,9 +49,9 @@ const plugin = (args: IpluginInputArgs): IpluginOutputArgs => { args.inputs = lib.loadDefaultValues(args.inputs, details); const extensions = String(args.inputs.extensions); - const extensionArray = extensions.trim().split(','); + const extensionArray = extensions.trim().split(',').map((row) => row.toLowerCase()); - const extension = getContainer(args.inputFileObj._id); + const extension = getContainer(args.inputFileObj._id).toLowerCase(); let extensionMatch = false; diff --git a/FlowPluginsTs/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.ts b/FlowPluginsTs/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.ts index f54ac4154..6d512b695 100644 --- a/FlowPluginsTs/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.ts +++ b/FlowPluginsTs/CommunityFlowPlugins/file/checkFileNameIncludes/1.0.0/index.ts @@ -23,12 +23,10 @@ const details = (): IpluginDetails => ({ label: 'Terms', name: 'terms', type: 'string', - // eslint-disable-next-line no-template-curly-in-string defaultValue: '_720p,_1080p', inputUI: { type: 'text', }, - // eslint-disable-next-line no-template-curly-in-string tooltip: 'Specify terms to check for in file name using comma seperated list e.g. _720p,_1080p', }, ], diff --git a/FlowPluginsTs/CommunityFlowPlugins/file/checkFileNameIncludes/2.0.0/index.ts b/FlowPluginsTs/CommunityFlowPlugins/file/checkFileNameIncludes/2.0.0/index.ts new file mode 100644 index 000000000..aba3ae020 --- /dev/null +++ b/FlowPluginsTs/CommunityFlowPlugins/file/checkFileNameIncludes/2.0.0/index.ts @@ -0,0 +1,105 @@ +import { getContainer, getFileName } from '../../../../FlowHelpers/1.0.0/fileUtils'; +import { + IpluginDetails, + IpluginInputArgs, + IpluginOutputArgs, +} from '../../../../FlowHelpers/1.0.0/interfaces/interfaces'; + +/* eslint no-plusplus: ["error", { "allowForLoopAfterthoughts": true }] */ +const details = (): IpluginDetails => ({ + name: 'Check File Name Includes', + description: 'Check if a file name includes specific terms. Only needs to match one term', + style: { + borderColor: 'orange', + }, + tags: 'video', + isStartPlugin: false, + pType: '', + requiresVersion: '2.11.01', + sidebarPosition: -1, + icon: 'faQuestion', + inputs: [ + { + label: 'Terms', + name: 'terms', + type: 'string', + defaultValue: '_720p,_1080p', + inputUI: { + type: 'text', + }, + tooltip: 'Specify terms to check for in file name using comma seperated list e.g. _720p,_1080p', + }, + { + label: 'Pattern (regular expression)', + name: 'pattern', + type: 'string', + defaultValue: '', + inputUI: { + type: 'text', + }, + tooltip: 'Specify the pattern (regex) to check for in file name e.g. ^Pattern.*mkv$', + }, + { + label: 'Include file directory in check', + name: 'includeFileDirectory', + type: 'boolean', + defaultValue: 'false', + inputUI: { + type: 'switch', + }, + tooltip: 'Should the terms and patterns be evaluated against the file directory e.g. false, true', + }, + ], + outputs: [ + { + number: 1, + tooltip: 'File name contains terms or patterns', + }, + { + number: 2, + tooltip: 'File name does not contain any of the terms or patterns', + }, + ], +}); + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const plugin = (args: IpluginInputArgs): IpluginOutputArgs => { + const lib = require('../../../../../methods/lib')(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign + args.inputs = lib.loadDefaultValues(args.inputs, details); + + const terms = String(args.inputs.terms); + const pattern = String(args.inputs.pattern); + const { includeFileDirectory } = args.inputs; + + const fileName = includeFileDirectory + ? args.inputFileObj._id + : `${getFileName(args.inputFileObj._id)}.${getContainer(args.inputFileObj._id)}`; + + const searchCriteriasArray = terms.trim().split(',') + .map((term) => term.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')); // https://github.com/tc39/proposal-regex-escaping + + if (pattern) { + searchCriteriasArray.push(pattern); + } + + const searchCriteriaMatched = searchCriteriasArray + .find((searchCriteria) => new RegExp(searchCriteria).test(fileName)); + const isAMatch = searchCriteriaMatched !== undefined; + + if (isAMatch) { + args.jobLog(`'${fileName}' includes '${searchCriteriaMatched}'`); + } else { + args.jobLog(`'${fileName}' does not include any of the terms or patterns`); + } + + return { + outputFileObj: args.inputFileObj, + outputNumber: isAMatch ? 1 : 2, + variables: args.variables, + }; +}; +export { + details, + plugin, +}; diff --git a/FlowPluginsTs/CommunityFlowPlugins/tools/applyRadarrOrSonarrNamingPolicy/1.0.0/index.ts b/FlowPluginsTs/CommunityFlowPlugins/tools/applyRadarrOrSonarrNamingPolicy/1.0.0/index.ts new file mode 100644 index 000000000..2cecd7618 --- /dev/null +++ b/FlowPluginsTs/CommunityFlowPlugins/tools/applyRadarrOrSonarrNamingPolicy/1.0.0/index.ts @@ -0,0 +1,293 @@ +import fileMoveOrCopy from '../../../../FlowHelpers/1.0.0/fileMoveOrCopy'; +import { + getContainer, getFileAbosluteDir, getFileName, +} from '../../../../FlowHelpers/1.0.0/fileUtils'; +import { + IpluginDetails, + IpluginInputArgs, + IpluginOutputArgs, +} from '../../../../FlowHelpers/1.0.0/interfaces/interfaces'; + +const details = (): IpluginDetails => ({ + name: 'Apply Radarr or Sonarr naming policy', + description: + 'Apply Radarr or Sonarr naming policy to a file. This plugin should be called after the original file has been ' + + 'replaced and Radarr or Sonarr has been notified. Radarr or Sonarr should also be notified after this plugin.', + style: { + borderColor: 'green', + }, + tags: '', + isStartPlugin: false, + pType: '', + requiresVersion: '2.11.01', + sidebarPosition: -1, + icon: 'faPenToSquare', + inputs: [ + { + label: 'Arr', + name: 'arr', + type: 'string', + defaultValue: 'radarr', + inputUI: { + type: 'dropdown', + options: ['radarr', 'sonarr'], + }, + tooltip: 'Specify which arr to use', + }, + { + label: 'Arr API Key', + name: 'arr_api_key', + type: 'string', + defaultValue: '', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr api key here', + }, + { + label: 'Arr Host', + name: 'arr_host', + type: 'string', + defaultValue: 'http://192.168.1.1:7878', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr host here.' + + '\\nExample:\\n' + + 'http://192.168.1.1:7878\\n' + + 'http://192.168.1.1:8989\\n' + + 'https://radarr.domain.com\\n' + + 'https://sonarr.domain.com\\n', + }, + ], + outputs: [ + { + number: 1, + tooltip: 'Radarr or Sonarr notified', + }, + { + number: 2, + tooltip: 'Radarr or Sonarr do not know this file', + }, + ], +}); + +interface IHTTPHeaders { + 'Content-Type': string, + 'X-Api-Key': string, + Accept: string, +} +interface IFileInfo { + id: string, + seasonNumber?: number, + episodeNumber?: number +} +interface ILookupResponse { + data: [{ id: number }], +} +interface IParseResponse { + data: { + movie?: { id: number }, + series?: { id: number }, + parsedEpisodeInfo?: { + episodeNumbers: number[], + seasonNumber: number + }, + }, +} +interface IFileToRename { + newPath: string, + episodeNumbers?: number[] +} +interface IPreviewRenameResponse { + data: IFileToRename[] +} +interface IArrApp { + name: string, + host: string, + headers: IHTTPHeaders, + content: string, + delegates: { + getFileInfoFromLookupResponse: + (lookupResponse: ILookupResponse, fileName: string) => IFileInfo, + getFileInfoFromParseResponse: + (parseResponse: IParseResponse) => IFileInfo, + buildPreviewRenameResquestUrl: + (fileInfo: IFileInfo) => string, + getFileToRenameFromPreviewRenameResponse: + (previewRenameResponse: IPreviewRenameResponse, fileInfo: IFileInfo) => IFileToRename | undefined + } +} + +const getFileInfoFromLookup = async ( + args: IpluginInputArgs, + arrApp: IArrApp, + fileName: string, +) + : Promise => { + let fInfo: IFileInfo = { id: '-1' }; + const imdbId = /\b(tt|nm|co|ev|ch|ni)\d{7,10}?\b/i.exec(fileName)?.at(0) ?? ''; + if (imdbId !== '') { + const lookupResponse: ILookupResponse = await args.deps.axios({ + method: 'get', + url: `${arrApp.host}/api/v3/${arrApp.name === 'radarr' ? 'movie' : 'series'}/lookup?term=imdb:${imdbId}`, + headers: arrApp.headers, + }); + fInfo = arrApp.delegates.getFileInfoFromLookupResponse(lookupResponse, fileName); + args.jobLog(`${arrApp.content} ${fInfo.id !== '-1' ? `'${fInfo.id}' found` : 'not found'}` + + ` for imdb '${imdbId}'`); + } + return fInfo; +}; + +const getFileInfoFromParse = async ( + args: IpluginInputArgs, + arrApp: IArrApp, + fileName: string, +) + : Promise => { + let fInfo: IFileInfo = { id: '-1' }; + const parseResponse: IParseResponse = await args.deps.axios({ + method: 'get', + url: `${arrApp.host}/api/v3/parse?title=${encodeURIComponent(getFileName(fileName))}`, + headers: arrApp.headers, + }); + fInfo = arrApp.delegates.getFileInfoFromParseResponse(parseResponse); + args.jobLog(`${arrApp.content} ${fInfo.id !== '-1' ? `'${fInfo.id}' found` : 'not found'}` + + ` for '${getFileName(fileName)}'`); + return fInfo; +}; + +const getFileInfo = async ( + args: IpluginInputArgs, + arrApp: IArrApp, + fileName: string, +) + : Promise => { + const fInfo = await getFileInfoFromLookup(args, arrApp, fileName); + return (fInfo.id === '-1' || (arrApp.name === 'sonarr' && (fInfo.seasonNumber === -1 || fInfo.episodeNumber === -1))) + ? getFileInfoFromParse(args, arrApp, fileName) + : fInfo; +}; + +const plugin = async (args: IpluginInputArgs): Promise => { + const lib = require('../../../../../methods/lib')(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign + args.inputs = lib.loadDefaultValues(args.inputs, details); + + let newPath = ''; + let isSuccessful = false; + const arr = String(args.inputs.arr); + const arr_host = String(args.inputs.arr_host).trim(); + const arrHost = arr_host.endsWith('/') ? arr_host.slice(0, -1) : arr_host; + const originalFileName = args.originalLibraryFile?._id ?? ''; + const currentFileName = args.inputFileObj?._id ?? ''; + const headers = { + 'Content-Type': 'application/json', + 'X-Api-Key': String(args.inputs.arr_api_key), + Accept: 'application/json', + }; + + const arrApp: IArrApp = arr === 'radarr' + ? { + name: arr, + host: arrHost, + headers, + content: 'Movie', + delegates: { + getFileInfoFromLookupResponse: + (lookupResponse) => ({ id: String(lookupResponse?.data?.at(0)?.id ?? -1) }), + getFileInfoFromParseResponse: + (parseResponse) => ({ id: String(parseResponse?.data?.movie?.id ?? -1) }), + buildPreviewRenameResquestUrl: + (fInfo) => `${arrHost}/api/v3/rename?movieId=${fInfo.id}`, + getFileToRenameFromPreviewRenameResponse: + (previewRenameResponse) => previewRenameResponse.data?.at(0), + }, + } + : { + name: arr, + host: arrHost, + headers, + content: 'Serie', + delegates: { + getFileInfoFromLookupResponse: + (lookupResponse, fileName) => { + const fInfo: IFileInfo = { id: String(lookupResponse?.data?.at(0)?.id ?? -1) }; + if (fInfo.id !== '-1') { + const seasonEpisodenumber = /\bS\d{1,3}E\d{1,4}\b/i.exec(fileName)?.at(0) ?? ''; + const episodeNumber = /\d{1,4}$/i.exec(seasonEpisodenumber)?.at(0) ?? ''; + fInfo.seasonNumber = Number(/\d{1,3}/i + .exec(seasonEpisodenumber.slice(0, -episodeNumber.length)) + ?.at(0) ?? '-1'); + fInfo.episodeNumber = Number(episodeNumber !== '' ? episodeNumber : -1); + } + return fInfo; + }, + getFileInfoFromParseResponse: + (parseResponse) => ({ + id: String(parseResponse?.data?.series?.id ?? -1), + seasonNumber: parseResponse?.data?.parsedEpisodeInfo?.seasonNumber ?? 1, + episodeNumber: parseResponse?.data?.parsedEpisodeInfo?.episodeNumbers?.at(0) ?? 1, + }), + buildPreviewRenameResquestUrl: + (fInfo) => `${arrHost}/api/v3/rename?seriesId=${fInfo.id}&seasonNumber=${fInfo.seasonNumber}`, + getFileToRenameFromPreviewRenameResponse: + (previewRenameResponse, fInfo) => previewRenameResponse.data + ?.find((episodeFile) => episodeFile.episodeNumbers?.at(0) === fInfo.episodeNumber), + }, + }; + + args.jobLog('Going to apply new name'); + args.jobLog(`Renaming ${arrApp.name}...`); + + // Retrieving movie or serie id, plus season and episode number for serie + let fInfo = await getFileInfo(args, arrApp, originalFileName); + // Useful in some edge cases + if (fInfo.id === '-1' && currentFileName !== originalFileName) { + fInfo = await getFileInfo(args, arrApp, currentFileName); + } + + // Checking that the file has been found + if (fInfo.id !== '-1') { + // Using rename endpoint to get ids of all the files that need renaming + const previewRenameRequestResult = await args.deps.axios({ + method: 'get', + url: arrApp.delegates.buildPreviewRenameResquestUrl(fInfo), + headers, + }); + const fileToRename = arrApp.delegates + .getFileToRenameFromPreviewRenameResponse(previewRenameRequestResult, fInfo); + + // Only if there is a rename to execute + if (fileToRename !== undefined) { + newPath = `${getFileAbosluteDir(currentFileName) + }/${getFileName(fileToRename.newPath) + }.${getContainer(fileToRename.newPath)}`; + + isSuccessful = await fileMoveOrCopy({ + operation: 'move', + sourcePath: currentFileName, + destinationPath: newPath, + args, + }); + } else { + isSuccessful = true; + args.jobLog('✔ No rename necessary.'); + } + } + + return { + outputFileObj: + isSuccessful && newPath !== '' + ? { ...args.inputFileObj, _id: newPath } + : args.inputFileObj, + outputNumber: isSuccessful ? 1 : 2, + variables: args.variables, + }; +}; + +export { + details, + plugin, +}; diff --git a/FlowPluginsTs/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.ts b/FlowPluginsTs/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.ts index 266283a74..844c6e16d 100644 --- a/FlowPluginsTs/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.ts +++ b/FlowPluginsTs/CommunityFlowPlugins/tools/checkFlowVariable/1.0.0/index.ts @@ -64,7 +64,8 @@ const details = (): IpluginDetails => ({ inputUI: { type: 'text', }, - tooltip: 'Value of variable to check', + tooltip: `Value of variable to check. +You can specify multiple values separated by comma. For example: value1,value2,value3`, }, ], outputs: [ @@ -122,20 +123,21 @@ const plugin = (args: IpluginInputArgs): IpluginOutputArgs => { targetValue = String(targetValue); let outputNumber = 1; + const valuesArr = value.trim().split(','); if (condition === '==') { - if (targetValue === value) { - args.jobLog(`Variable ${variable} of value ${targetValue} matches condition ${condition} ${value}`); + if (valuesArr.includes(targetValue)) { + args.jobLog(`Variable ${variable} of value ${targetValue} matches condition ${condition} ${valuesArr}`); outputNumber = 1; } else { - args.jobLog(`Variable ${variable} of value ${targetValue} does not match condition ${condition} ${value}`); + args.jobLog(`Variable ${variable} of value ${targetValue} does not match condition ${condition} ${valuesArr}`); outputNumber = 2; } } else if (condition === '!=') { - if (targetValue !== value) { - args.jobLog(`Variable ${variable} of value ${targetValue} matches condition ${condition} ${value}`); + if (!valuesArr.includes(targetValue)) { + args.jobLog(`Variable ${variable} of value ${targetValue} matches condition ${condition} ${valuesArr}`); outputNumber = 1; } else { - args.jobLog(`Variable ${variable} of value ${targetValue} does not match condition ${condition} ${value}`); + args.jobLog(`Variable ${variable} of value ${targetValue} does not match condition ${condition} ${valuesArr}`); outputNumber = 2; } } diff --git a/FlowPluginsTs/CommunityFlowPlugins/tools/notifyRadarrOrSonarr/2.0.0/index.ts b/FlowPluginsTs/CommunityFlowPlugins/tools/notifyRadarrOrSonarr/2.0.0/index.ts new file mode 100644 index 000000000..9629101b5 --- /dev/null +++ b/FlowPluginsTs/CommunityFlowPlugins/tools/notifyRadarrOrSonarr/2.0.0/index.ts @@ -0,0 +1,196 @@ +import { getFileName } from '../../../../FlowHelpers/1.0.0/fileUtils'; +import { + IpluginDetails, + IpluginInputArgs, + IpluginOutputArgs, +} from '../../../../FlowHelpers/1.0.0/interfaces/interfaces'; + +const details = (): IpluginDetails => ({ + name: 'Notify Radarr or Sonarr', + description: 'Notify Radarr or Sonarr to refresh after file change', + style: { + borderColor: 'green', + }, + tags: '', + isStartPlugin: false, + pType: '', + requiresVersion: '2.11.01', + sidebarPosition: -1, + icon: 'faBell', + inputs: [ + { + label: 'Arr', + name: 'arr', + type: 'string', + defaultValue: 'radarr', + inputUI: { + type: 'dropdown', + options: ['radarr', 'sonarr'], + }, + tooltip: 'Specify which arr to use', + }, + { + label: 'Arr API Key', + name: 'arr_api_key', + type: 'string', + defaultValue: '', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr api key here', + }, + { + label: 'Arr Host', + name: 'arr_host', + type: 'string', + defaultValue: 'http://192.168.1.1:7878', + inputUI: { + type: 'text', + }, + tooltip: 'Input your arr host here.' + + '\\nExample:\\n' + + 'http://192.168.1.1:7878\\n' + + 'http://192.168.1.1:8989\\n' + + 'https://radarr.domain.com\\n' + + 'https://sonarr.domain.com\\n', + }, + ], + outputs: [ + { + number: 1, + tooltip: 'Radarr or Sonarr notified', + }, + { + number: 2, + tooltip: 'Radarr or Sonarr do not know this file', + }, + ], +}); + +interface IHTTPHeaders { + 'Content-Type': string, + 'X-Api-Key': string, + Accept: string, +} +interface IParseResponse { + data: { + movie?: { id: number }, + series?: { id: number }, + }, +} +interface IArrApp { + name: string, + host: string, + headers: IHTTPHeaders, + content: string, + delegates: { + getIdFromParseResponse: (parseResponse: IParseResponse) => number, + buildRefreshResquestData: (id: number) => string + } +} + +const getId = async ( + args: IpluginInputArgs, + arrApp: IArrApp, + fileName: string, +) + : Promise => { + const imdbId = /\b(tt|nm|co|ev|ch|ni)\d{7,10}?\b/i.exec(fileName)?.at(0) ?? ''; + let id = (imdbId !== '') + ? Number((await args.deps.axios({ + method: 'get', + url: `${arrApp.host}/api/v3/${arrApp.name === 'radarr' ? 'movie' : 'series'}/lookup?term=imdb:${imdbId}`, + headers: arrApp.headers, + })).data?.at(0)?.id ?? -1) + : -1; + args.jobLog(`${arrApp.content} ${id !== -1 ? `'${id}' found` : 'not found'} for imdb '${imdbId}'`); + if (id === -1) { + id = arrApp.delegates.getIdFromParseResponse( + (await args.deps.axios({ + method: 'get', + url: `${arrApp.host}/api/v3/parse?title=${encodeURIComponent(getFileName(fileName))}`, + headers: arrApp.headers, + })), + ); + args.jobLog(`${arrApp.content} ${id !== -1 ? `'${id}' found` : 'not found'} for '${getFileName(fileName)}'`); + } + return id; +}; + +const plugin = async (args: IpluginInputArgs): Promise => { + const lib = require('../../../../../methods/lib')(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign + args.inputs = lib.loadDefaultValues(args.inputs, details); + + // Variables initialization + let refreshed = false; + const arr = String(args.inputs.arr); + const arr_host = String(args.inputs.arr_host).trim(); + const arrHost = arr_host.endsWith('/') ? arr_host.slice(0, -1) : arr_host; + const originalFileName = args.originalLibraryFile?._id ?? ''; + const currentFileName = args.inputFileObj?._id ?? ''; + const headers: IHTTPHeaders = { + 'Content-Type': 'application/json', + 'X-Api-Key': String(args.inputs.arr_api_key), + Accept: 'application/json', + }; + const arrApp: IArrApp = arr === 'radarr' + ? { + name: arr, + host: arrHost, + headers, + content: 'Movie', + delegates: { + getIdFromParseResponse: + (parseResponse: IParseResponse) => Number(parseResponse?.data?.movie?.id ?? -1), + buildRefreshResquestData: + (id) => JSON.stringify({ name: 'RefreshMovie', movieIds: [id] }), + }, + } + : { + name: arr, + host: arrHost, + headers, + content: 'Serie', + delegates: { + getIdFromParseResponse: + (parseResponse: IParseResponse) => Number(parseResponse?.data?.series?.id ?? -1), + buildRefreshResquestData: + (id) => JSON.stringify({ name: 'RefreshSeries', seriesId: id }), + }, + }; + + args.jobLog('Going to force scan'); + args.jobLog(`Refreshing ${arrApp.name}...`); + + let id = await getId(args, arrApp, originalFileName); + // Useful in some edge cases + if (id === -1 && currentFileName !== originalFileName) { + id = await getId(args, arrApp, currentFileName); + } + + // Checking that the file has been found + if (id !== -1) { + // Using command endpoint to queue a refresh task + await args.deps.axios({ + method: 'post', + url: `${arrApp.host}/api/v3/command`, + headers, + data: arrApp.delegates.buildRefreshResquestData(id), + }); + + refreshed = true; + args.jobLog(`✔ ${arrApp.content} '${id}' refreshed in ${arrApp.name}.`); + } + + return { + outputFileObj: args.inputFileObj, + outputNumber: refreshed ? 1 : 2, + variables: args.variables, + }; +}; + +export { + details, + plugin, +}; diff --git a/tests/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js b/tests/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js index 14c773435..37da81f0a 100644 --- a/tests/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js +++ b/tests/Community/Tdarr_Plugin_bsh1_Boosh_FFMPEG_QSV_HEVC.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const run = require('../helpers/run'); const tests = [ + // Test 0 { input: { file: _.cloneDeep(require('../sampleData/media/sampleH264_1.json')), @@ -13,58 +14,55 @@ const tests = [ output: { linux: { processFile: true, - preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv \n' - + ' -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset slow -c:a copy -c:s copy -max_muxing_queue_size 9999 ', + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset slow -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -vf hwupload=extra_hw_frames=64,format=qsv ', handBrakeMode: false, FFmpegMode: true, reQueueAfter: true, - infoLog: 'Input file is not MKV so cannot use mkvpropedit to get new file stats. Continuing but file stats will likely be inaccurate...\n' - + '☑ It looks like the current video bitrate is 1206kbps. \n' - + 'Container for output selected as mkv. \n' - + 'Encode variable bitrate settings: \n' - + 'Target = 603k \n' - + 'Minimum = 452k \n' - + 'Maximum = 754k \n' - + 'File Transcoding... \n', + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', container: '.mkv', }, win32: { processFile: true, - preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv \n' - + ' -init_hw_device qsv:hw_any,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -load_plugin hevc_hw -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset slow -c:a copy -c:s copy -max_muxing_queue_size 9999 ', + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset slow -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -vf hwupload=extra_hw_frames=64,format=qsv ', handBrakeMode: false, FFmpegMode: true, reQueueAfter: true, - infoLog: 'Input file is not MKV so cannot use mkvpropedit to get new file stats. Continuing but file stats will likely be inaccurate...\n' - + '☑ It looks like the current video bitrate is 1206kbps. \n' - + 'Container for output selected as mkv. \n' - + 'Encode variable bitrate settings: \n' - + 'Target = 603k \n' - + 'Minimum = 452k \n' - + 'Maximum = 754k \n' - + 'File Transcoding... \n', + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', container: '.mkv', }, darwin: { processFile: true, - preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset slow -c:a copy -c:s copy -max_muxing_queue_size 9999 ', + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset slow -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v 2 -pix_fmt yuv420p10le ', handBrakeMode: false, FFmpegMode: true, reQueueAfter: true, - infoLog: 'Input file is not MKV so cannot use mkvpropedit to get new file stats. Continuing but file stats will likely be inaccurate...\n' - + '☑ It looks like the current video bitrate is 1206kbps. \n' - + 'Container for output selected as mkv. \n' - + 'Encode variable bitrate settings: \n' - + 'Target = 603k \n' - + 'Minimum = 452k \n' - + 'Maximum = 754k \n' + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + 'cmds set in extra_qsv_options will be IGNORED!\n' - + 'File Transcoding... \n', + + 'File Transcoding...\n', container: '.mkv', }, }, }, + // Test 1 { input: { file: _.cloneDeep(require('../sampleData/media/sampleH264_1.json')), @@ -79,70 +77,222 @@ const tests = [ output: { linux: { processFile: true, - preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv \n' - + ' -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -profile:v main10 -vf scale_qsv=format=p010le ', + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f mp4 -profile:v main10 -vf scale_qsv=format=p010le,hwupload=extra_hw_frames=64,format=qsv ', handBrakeMode: false, FFmpegMode: true, reQueueAfter: true, - infoLog: 'Input file is not MKV so cannot use mkvpropedit to get new file stats. Continuing but file stats will likely be inaccurate...\n' - + '☑ It looks like the current video bitrate is 1206kbps. \n' - + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format \n' - + 'Container for output selected as mp4. \n' - + 'Encode variable bitrate settings: \n' - + 'Target = 603k \n' - + 'Minimum = 452k \n' - + 'Maximum = 754k \n' - + 'File Transcoding... \n', + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mp4.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', container: '.mp4', }, win32: { processFile: true, - preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv \n' - + ' -init_hw_device qsv:hw_any,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -load_plugin hevc_hw -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -profile:v main10 -vf scale_qsv=format=p010le ', + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f mp4 -profile:v main10 -vf scale_qsv=format=p010le,hwupload=extra_hw_frames=64,format=qsv ', handBrakeMode: false, FFmpegMode: true, reQueueAfter: true, - infoLog: 'Input file is not MKV so cannot use mkvpropedit to get new file stats. Continuing but file stats will likely be inaccurate...\n' - + '☑ It looks like the current video bitrate is 1206kbps. \n' - + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format \n' - + 'Container for output selected as mp4. \n' - + 'Encode variable bitrate settings: \n' - + 'Target = 603k \n' - + 'Minimum = 452k \n' - + 'Maximum = 754k \n' - + 'File Transcoding... \n', + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mp4.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', container: '.mp4', }, darwin: { processFile: true, - preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -profile:v main10 -vf scale_qsv=format=p010le ', + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f mp4 -profile:v 2 -pix_fmt yuv420p10le ', handBrakeMode: false, FFmpegMode: true, reQueueAfter: true, - infoLog: 'Input file is not MKV so cannot use mkvpropedit to get new file stats. Continuing but file stats will likely be inaccurate...\n' - + '☑ It looks like the current video bitrate is 1206kbps. \n' - + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format \n' - + 'Container for output selected as mp4. \n' - + 'Encode variable bitrate settings: \n' - + 'Target = 603k \n' - + 'Minimum = 452k \n' - + 'Maximum = 754k \n' + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mp4.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + 'cmds set in extra_qsv_options will be IGNORED!\n' - + 'File Transcoding... \n', + + 'File Transcoding...\n', container: '.mp4', }, }, }, + // Test 2 { input: { - file: _.cloneDeep(require('../sampleData/media/sampleH264_1.json')), + file: (() => { + const file = _.cloneDeep(require('../sampleData/media/sampleH264_2.json')); + file.ffProbeData.streams[0].profile = 'High 10'; + return file; + })(), librarySettings: {}, inputs: { - container: 'mp4', + container: 'mkv', encoder_speedpreset: 'fast', - enable_10bit: 'true', - bitrate_cutoff: '2000', + }, + otherArguments: {}, + }, + output: { + linux: { + processFile: true, + preset: '-fflags +genpts -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264 -map 0 -c:v hevc_qsv -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10 -pix_fmt p010le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + 'Input file is h264 High10. Hardware Decode not supported.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + win32: { + processFile: true, + preset: '-fflags +genpts -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v h264 -map 0 -c:v hevc_qsv -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10 -pix_fmt p010le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + 'Input file is h264 High10. Hardware Decode not supported.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + darwin: { + processFile: true, + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v 2 -pix_fmt yuv420p10le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + 'Input file is h264 High10. Hardware Decode not supported.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + + 'cmds set in extra_qsv_options will be IGNORED!\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + }, + }, + // Test 3 + { + input: { + file: (() => { + const file = _.cloneDeep(require('../sampleData/media/sampleH265_1.json')); + file.mediaInfo.track[1].BitRate = 12000000; + file.ffProbeData.streams[0].profile = 'Main 10'; + return file; + })(), + librarySettings: {}, + inputs: { + container: 'mkv', + encoder_speedpreset: 'fast', + reconvert_hevc: 'true', + hevc_max_bitrate: '6000', + bitrate_cutoff: '4000', + }, + otherArguments: {}, + }, + output: { + linux: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v hevc_qsv -map 0 -c:v hevc_qsv -b:v 6000k -minrate 4500k -maxrate 7500k -bufsize 12000k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10 -vf scale_qsv=format=p010le,hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 12000kbps.\n' + + 'Reconvert_hevc is true & the file is already HEVC, VP9 or AV1. Using HEVC specific cutoff of 6000kbps.\n' + + '☒ The file is still above this new cutoff! Reconverting.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 6000k\n' + + 'Minimum = 4500k\n' + + 'Maximum = 7500k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + win32: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v hevc_qsv -map 0 -c:v hevc_qsv -b:v 6000k -minrate 4500k -maxrate 7500k -bufsize 12000k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10 -vf scale_qsv=format=p010le,hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 12000kbps.\n' + + 'Reconvert_hevc is true & the file is already HEVC, VP9 or AV1. Using HEVC specific cutoff of 6000kbps.\n' + + '☒ The file is still above this new cutoff! Reconverting.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 6000k\n' + + 'Minimum = 4500k\n' + + 'Maximum = 7500k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + darwin: { + processFile: true, + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 6000k -minrate 4500k -maxrate 7500k -bufsize 12000k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v 2 -pix_fmt yuv420p10le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 12000kbps.\n' + + 'Reconvert_hevc is true & the file is already HEVC, VP9 or AV1. Using HEVC specific cutoff of 6000kbps.\n' + + '☒ The file is still above this new cutoff! Reconverting.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 6000k\n' + + 'Minimum = 4500k\n' + + 'Maximum = 7500k\n' + + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + + 'cmds set in extra_qsv_options will be IGNORED!\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + }, + }, + // Test 4 + { + input: { + file: (() => { + const file = _.cloneDeep(require('../sampleData/media/sampleH265_1.json')); + file.mediaInfo.track[1].BitRate = 5000000; + file.ffProbeData.streams[0].profile = 'Main 10'; + file.mediaInfo.track[0].extra.JBDONEDATE = new Date().toISOString(); + return file; + })(), + librarySettings: {}, + inputs: { + container: 'mkv', + encoder_speedpreset: 'fast', + reconvert_hevc: 'true', + hevc_max_bitrate: '6000', + bitrate_cutoff: '4000', }, otherArguments: {}, }, @@ -152,13 +302,366 @@ const tests = [ handBrakeMode: false, FFmpegMode: true, reQueueAfter: true, - infoLog: 'Input file is not MKV so cannot use mkvpropedit to get new file stats. Continuing but file stats will likely be inaccurate...\n' - + '☑ It looks like the current video bitrate is 1206kbps. \n' - + '☑ Current bitrate is below set cutoff of 2000kbps. \n' - + 'Cancelling plugin. \n', - container: '.mp4', + infoLog: '☑ It looks like the current video bitrate is 5000kbps.\n' + + 'Reconvert_hevc is true & the file is already HEVC, VP9 or AV1. Using HEVC specific cutoff of 6000kbps.\n' + + '☑ The file is NOT above this new cutoff. Exiting plugin.\n', + container: '.mkv', + }, + }, + // Test 5 + { + input: { + file: (() => { + const file = _.cloneDeep(require('../sampleData/media/sampleH264_2.json')); + file.ffProbeData.streams[3].codec_name = 'hdmv_pgs_subtitle'; + file.ffProbeData.streams[4].codec_name = 'eia_608'; + file.ffProbeData.streams[5].codec_name = 'subrip'; + file.ffProbeData.streams[6].codec_name = 'timed_id3'; + return file; + })(), + librarySettings: {}, + inputs: { + container: 'mp4', + encoder_speedpreset: 'fast', + force_conform: 'true', + }, + otherArguments: {}, + }, + output: { + linux: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -map -0:3 -map -0:4 -map -0:5 -map -0:6 -f mp4 -vf hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + 'Container for output selected as mp4.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + 'File Transcoding...\n', + container: '.mp4', + }, + win32: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -map -0:3 -map -0:4 -map -0:5 -map -0:6 -f mp4 -vf hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + 'Container for output selected as mp4.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + 'File Transcoding...\n', + container: '.mp4', + }, + darwin: { + processFile: true, + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -map -0:3 -map -0:4 -map -0:5 -map -0:6 -f mp4 -profile:v 2 -pix_fmt yuv420p10le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mp4.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + + 'cmds set in extra_qsv_options will be IGNORED!\n' + + 'File Transcoding...\n', + container: '.mp4', + }, + }, + }, + // Test 6 + { + input: { + file: (() => { + const file = _.cloneDeep(require('../sampleData/media/sampleH264_2.json')); + file.ffProbeData.streams[3].codec_name = 'mov_text'; + file.ffProbeData.streams[4].codec_name = 'eia_608'; + file.ffProbeData.streams[5].codec_name = 'timed_id3'; + return file; + })(), + librarySettings: {}, + inputs: { + container: 'mkv', + encoder_speedpreset: 'fast', + force_conform: 'true', + }, + otherArguments: {}, + }, + output: { + linux: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -map -0:d -map -0:3 -map -0:4 -map -0:5 -f matroska -vf hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + win32: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -map -0:d -map -0:3 -map -0:4 -map -0:5 -f matroska -vf hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + darwin: { + processFile: true, + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 3227k -minrate 2420k -maxrate 4034k -bufsize 6454k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -map -0:d -map -0:3 -map -0:4 -map -0:5 -f matroska -profile:v 2 -pix_fmt yuv420p10le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 6454kbps.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 3227k\n' + + 'Minimum = 2420k\n' + + 'Maximum = 4034k\n' + + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + + 'cmds set in extra_qsv_options will be IGNORED!\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + }, + }, + // Test 7 + { + input: { + file: _.cloneDeep(require('../sampleData/media/sampleH264_1.json')), + librarySettings: {}, + inputs: { + container: 'mkv', + encoder_speedpreset: 'fast', + extra_qsv_options: '-look_ahead 1 -look_ahead_depth 100 -extbrc 1 -rdo 1 -mbbrc 1 -b_strategy 1 -adaptive_i 1 -adaptive_b 1 -vf scale_qsv=1280:-1', + enable_10bit: 'true', + }, + otherArguments: {}, + }, + output: { + linux: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -look_ahead 1 -look_ahead_depth 100 -extbrc 1 -rdo 1 -mbbrc 1 -b_strategy 1 -adaptive_i 1 -adaptive_b 1 -vf scale_qsv=1280:-1,format=p010le,hwupload=extra_hw_frames=64,format=qsv -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + win32: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -look_ahead 1 -look_ahead_depth 100 -extbrc 1 -rdo 1 -mbbrc 1 -b_strategy 1 -adaptive_i 1 -adaptive_b 1 -vf scale_qsv=1280:-1,format=p010le,hwupload=extra_hw_frames=64,format=qsv -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + darwin: { + processFile: true, + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v 2 -pix_fmt yuv420p10le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + + 'cmds set in extra_qsv_options will be IGNORED!\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + }, + }, + // Test 8 + { + input: { + file: (() => { + const file = _.cloneDeep(require('../sampleData/media/sampleH264_1.json')); + file.ffProbeData.streams[0].bits_per_raw_sample = '10'; + file.video_codec_name = 'vc1'; + return file; + })(), + librarySettings: {}, + inputs: { + container: 'mkv', + encoder_speedpreset: 'fast', + }, + otherArguments: {}, + }, + output: { + linux: { + processFile: true, + preset: '-fflags +genpts -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v vc1 -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10 -pix_fmt p010le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + 'Input file is vc1. Hardware Decode not supported.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + win32: { + processFile: true, + preset: '-fflags +genpts -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v vc1 -map 0 -c:v hevc_qsv -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v main10 -pix_fmt p010le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + 'Input file is vc1. Hardware Decode not supported.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + darwin: { + processFile: true, + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 603k -minrate 452k -maxrate 754k -bufsize 1206k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -f matroska -profile:v 2 -pix_fmt yuv420p10le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 1206kbps.\n' + + 'Input file is vc1. Hardware Decode not supported.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 603k\n' + + 'Minimum = 452k\n' + + 'Maximum = 754k\n' + + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + + 'cmds set in extra_qsv_options will be IGNORED!\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + }, + }, + // Test 9 + { + input: { + file: (() => { + const file = _.cloneDeep(require('../sampleData/media/sampleH264_1.json')); + file.ffProbeData.streams[0].color_space = 'bt2020nc'; + file.ffProbeData.streams[0].color_transfer = 'smpte2084'; + file.ffProbeData.streams[0].color_primaries = 'bt2020'; + file.mediaInfo.track[1].BitRate = 12000000; + file.ffProbeData.streams[0].profile = 'Main 10'; + return file; + })(), + librarySettings: {}, + inputs: { + container: 'mkv', + encoder_speedpreset: 'fast', + reconvert_hevc: 'true', + hevc_max_bitrate: '6000', + bitrate_cutoff: '4000', + }, + otherArguments: {}, + }, + output: { + linux: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw_any,child_device_type=vaapi -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 6000k -minrate 4500k -maxrate 7500k -bufsize 12000k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -f matroska -profile:v main10 -vf scale_qsv=format=p010le,hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 12000kbps.\n' + + '==WARNING== This looks to be a HDR file. HDR is supported but correct encoding is not guaranteed.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 6000k\n' + + 'Minimum = 4500k\n' + + 'Maximum = 7500k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + win32: { + processFile: true, + preset: '-fflags +genpts -hwaccel qsv -hwaccel_output_format qsv -init_hw_device qsv:hw,child_device_type=d3d11va -c:v h264_qsv -map 0 -c:v hevc_qsv -b:v 6000k -minrate 4500k -maxrate 7500k -bufsize 12000k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -f matroska -profile:v main10 -vf scale_qsv=format=p010le,hwupload=extra_hw_frames=64,format=qsv ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 12000kbps.\n' + + '==WARNING== This looks to be a HDR file. HDR is supported but correct encoding is not guaranteed.\n' + + '10 bit encode enabled. Setting Main10 Profile & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 6000k\n' + + 'Minimum = 4500k\n' + + 'Maximum = 7500k\n' + + 'File Transcoding...\n', + container: '.mkv', + }, + darwin: { + processFile: true, + preset: '-fflags +genpts -hwaccel videotoolbox -map 0 -c:v hevc_videotoolbox -b:v 6000k -minrate 4500k -maxrate 7500k -bufsize 12000k -preset fast -c:a copy -c:s copy -max_muxing_queue_size 9999 -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -f matroska -profile:v 2 -pix_fmt yuv420p10le ', + handBrakeMode: false, + FFmpegMode: true, + reQueueAfter: true, + infoLog: '☑ It looks like the current video bitrate is 12000kbps.\n' + + '==WARNING== This looks to be a HDR file. HDR is supported but correct encoding is not guaranteed.\n' + + '10 bit encode enabled. Setting VideoToolBox Profile v2 & 10 bit pixel format\n' + + 'Container for output selected as mkv.\n' + + 'Encode variable bitrate settings:\n' + + 'Target = 6000k\n' + + 'Minimum = 4500k\n' + + 'Maximum = 7500k\n' + + '==ALERT== OS detected as MAC - This will use VIDEOTOOLBOX to encode which is NOT QSV\n' + + 'cmds set in extra_qsv_options will be IGNORED!\n' + + 'File Transcoding...\n', + container: '.mkv', + }, }, }, ]; - void run(tests); diff --git a/tests/helpers/run.js b/tests/helpers/run.js index 2aba038da..cfe67ba3e 100644 --- a/tests/helpers/run.js +++ b/tests/helpers/run.js @@ -16,8 +16,10 @@ const stackLog = (err) => { }; const run = async (tests) => { - try { - for (let i = 0; i < tests.length; i += 1) { + let errorsEncountered = false; + + for (let i = 0; i < tests.length; i += 1) { + try { // eslint-disable-next-line no-console console.log(`[${os.platform()}] ${scriptName}: test ${i}`); const test = tests[i]; @@ -69,10 +71,14 @@ const run = async (tests) => { chai.assert.deepEqual(testOutput, expectedOutput); } } + } catch (err) { + // eslint-disable-next-line no-console + stackLog(err); + errorsEncountered = true; } - } catch (err) { - // eslint-disable-next-line no-console - stackLog(err); + } + + if (errorsEncountered) { process.exit(1); } };