header={ "chef": "BeatRig", "recipe_version": "1.55", "title": "Convert to ProRes Video", "description": "Convert (almost) any video to an Apple ProRes codec", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "palette": "Clean Slate", "category": "video", "flavour": "+i0QmXHYPJ8lB5bZRTTW6ZR1m5YGrh3CsRb1PbRDkQONxN/oGyANJzEzDopvw2eET2nercbXDwIFq9Ntl2SvA2EiJPhcY7IgXWbkaU6W9dPxAfjl1fiC2EYzPe/djA4tTFKWC59x45kZThR/VsHzuzLWX9fi8mzlzguzV0do+GU=", "time": 1736348891, "core_version": "0.7.2", "magnetron_version": "1.0.333", "dependencies": "ffmpeg,ffprobe", "tags": "ProRes", "os": "windows,macOS", "functions": "main,onConfig,onAbout", "uuid": "7ac9517dbb53433683da329326796829", "type": "default", "instructions": "Drop file(s) here" }; // ============================================================================= var settings = []; var framecount = 0; // ============================================================================= // SET DEFAULT VALUES function init(){ // LOCATION OF THE SETTINGS FILE settingsFile = folders.content+gvar.pss+'default.txt'; // SETTINGS settings = { "overwrite" : false, // overwrite existing files "customFolder" : false, // Move fixed files to a custom folder "customFolder_path" : folders.home, // By default this is the users home folder "nametag" : true, // Tag added to fixed file "nametag_val" : '-[prores]', // Name tag "subFolder" : false, // Move fixed files to a subfolder "subFolder_val" : 'prores', // Name of the subfolder }; } // ============================================================================= function ffcall(infile, outfile, specs) { // FFMPEG COMMAND return cmd("ffmpeg", [ // INPUT FILE '-i', infile, // INPUT FILE '-map', 'v:0', // // IF PRESENT ADD AUDIO, OR DISABLE AUDIO ...( specs.hasAudio ? ['-map', 'a:0', '-c:a', 'pcm_s24le'] // AUDIO : ['-an']), // NO AUDIO // VIDEO OUTPUT SETTINGS '-async','1', '-c:v','prores', '-r', specs.framerate, // FRAMERATE '-s','1920x1080', '-aspect','16:9', '-pix_fmt','yuv422p10le', '-profile:v','2', '-vendor','apl0', '-trellis','0', '-subq','7', '-me_range','16', '-b_strategy','1', '-sc_threshold','40', '-qmin','3', '-qmax','51', // OUTPUT outfile, // OUTPUT FILE '-y', // OVERWRITE // LOGGING '-loglevel', 'error', '-progress', 'pipe:1', '-nostats', '-hide_banner' ]); } // ============================================================================= // VidConvert 0.7.2r02 // ============================================================================= function main() { // ----------------------------- // DEFAULT SETTINGS init(); // ----------------------------- // CUSTOM SETTINGS loadSettings(); // ----------------------------- // MESSAGE setMainMessage("preparing"); // ----------------------------- // CHECK IF FFMPEG AND FFPROBE ARE AVAILABLE if(getAllowedApps("ffmpeg") == '' ) abort("FFMPEG is not available"); if(getAllowedApps("ffprobe") == '' ) abort("FFMPEG is not available"); // ----------------------------- // GET ALL FILES FROM USER INPUT var files = getFiles(); // ----------------------------- // IF NO FILES, WARN AND STOP if( files.length == 0) abort("Add some files first!"); // ----------------------------- // RESET THE META DATA files = files.map(file => ({ idpath: file.idpath, path: file.path, index: file.index, file_icon: "square", file_icon_color: "FFdddddd", file_status: "", file_tooltip: "", group: "" })); // ----------------------------- // UPDATE USER INTERFACE setFiles(files); // ----------------------------- // LOOP FILES for (const [k, v] of Object.entries(files)) { // ---------------- if(isCanceled()) break; // ---------------- setMainMessage("preparing file"); setProgress(101); // ---------------- var file = v.path; var specs = filespecs(file); // TEST IF VIDEO STREAM IS PRESENT if(!specs.hasVideo) { setFileIcon(v.path, v.index, "ExclamationSquare") setFileIconColor(v.path, v.index, "FFaa0000") setFileStatus(v.path, v.index, "ERROR - Could not find video stream") continue; } // SET STATUS setFileIcon(v.path, v.index, "CaretSquareORight") setFileStatus(v.path, v.index, "processing") // MAKE TOTAL FRAMCOUNT GLOBAL AVAILABLE FOR PROGRESS framecount = specs.framecount; // OUTFILE var path = getPathInfo(file); var outpath = ""; if(settings.customFolder){ outpath = settings.customFolder_path; } else { outpath = path.folder; } // SUBFOLDER if(settings.subFolder){ outpath = outpath+gvar.pss+settings.subFolder_val; } // CREATE FOLDER IF NOT EXISTS if(!fileExists(outpath)){ makeFolder(outpath); } // OUTPUT FILE var outfile = outpath + gvar.pss+path.basename + (settings.nametag ? settings.nametag_val : "") + ".mov"; if(fileExists(outfile)) { if(!settings.overwrite) { setFileIcon(v.path, v.index, "ExclamationSquare") setFileIconColor(v.path, v.index, "FFaa0000") setFileStatus(v.path, v.index, "ERROR - File exists") continue; } } // CALL FFMPEG ffcall(v.path, outfile, specs); // CHECK IF OUTPUT FILE WAS CREATED if(fileExists(outfile) && getFileBytes(outfile) > 0) { setFileIcon(v.path, v.index, "CheckSquare") setFileIconColor(v.path, v.index, "FF00aa00") setFileStatus(v.path, v.index, "DONE") } else { if(fileExists(outfile)) { deleteFile(outfile); }; setFileIcon(v.path, v.index, "ExclamationSquare") setFileIconColor(v.path, v.index, "FFaa0000") setFileStatus(v.path, v.index, "ERROR") } // THIS FILE IS DONE setProgress(100); setMainMessage("file done"); // ---------------- } // ALL IS DONE setMainMessage("all done"); // ---------------- } // ============================================================================= // FFMPEG PROCESS STATUS // ============================================================================= function cmd_callback(cmd_name, cmd_output) { if( cmd_name=="ffmpeg" && cmd_output.length > 0 && framecount > 0) { const matches = cmd_output.match(/frame=\s*\d+/g); const lastFrame = matches ? +matches.pop().split('=')[1] : null; progress = Math.round( lastFrame/framecount*100 ); setProgress(progress); setMainMessage("processing "+progress+"%"); } return { "terminate": isCanceled(), "input": "" }; } // ============================================================================= // FILE SPECS // ============================================================================= function filespecs(file) { const ffprobe = cmd("ffprobe", [ "-v", "error", "-print_format", "json", "-show_entries", "stream=codec_type,pix_fmt,width,height,r_frame_rate,codec_name,channels,sample_rate,bits_per_sample,duration", "-i", file ]); const specs = JSON.parse(ffprobe); const streams = specs.streams || []; const videoStream = streams.find(stream => stream.codec_type === "video"); const duration = parseFloat(videoStream?.duration) || null; const framerate = videoStream?.r_frame_rate ? eval(videoStream.r_frame_rate) : null; const framecount = duration && framerate ? Math.round(duration * framerate) : null; const hasVideo = streams.some(stream => stream.codec_type === "video"); const hasAudio = streams.some(stream => stream.codec_type === "audio"); return { framecount, framerate, hasVideo, hasAudio }; // ---------------- } // ============================================================================= // ON CONFIG, SHOW DIALOG AND SAVE SETTINGS function onConfig() { if(gvar.demo){ } else{ // ----------------------------- // DEFAULT SETTINGS init(); // ----------------------------- // LOAD CUSTOM SETTINGS loadSettings(); // ----------------------------- // IF THE CUSTOM PATH IS INVALID, SET IT TO HOME FOLDER if(!fileExists(settings.customFolder_path)){ settings.customFolder_path = folders.home; } // ----------------------------- // SETTINGS DIALOG var r = showSettings(); // ----------------------------- // IF RESET IS PUSHED if(r.returns === -1) { deleteFile(settingsFile); // DELETE THE SETTINGS FILE init(); // RESET TO DEFAULT SETTINGS onConfig(); // OPEN THIS CONFIG FUNCTION AGAIN } // ----------------------------- // IF SAVED WAS PUSHED else if(r.returns === 1) { // CHECK THAT SUBFOLDER DOES NOT CONTAIN SLASHES settings.subFolder_val = settings.subFolder_val.replace(/\//g, ''); writeFile( settingsFile, JSON.stringify(settings) ); } // ----------------------------- } } // ============================================================================= // ON ABOUT, SHOW POPUP WITH BUTTON TO OPEN WEBSITE function onAbout() { var args = { "info": { "type": "texteditor", "bounds" : { "y" : 70, "w" : 400, "h" : 150 }, "visible": true, "multiline" : true, "wordwrap" : true, "default" : header.description+'\n\nMore information on the recipes page on our website.', "enabled" : false }, "showpage" : { "type" : "button", "label" : "show page", "returns" : 1 }, "close" : { "type" : "button", "label" : "close", "returns" : 0 } }; var r = dialog(header.title, "" , "", args); if (r.showpage == 1) launchInBrowser('https://magnetron.app/recipes/uuid/'+header.uuid+'/'); } // ============================================================================= // LOAD DEFAULT VALUES function loadSettings() { if (fileExists(settingsFile)) { Object.assign(settings, JSON.parse(readFile(settingsFile))); } return true; } // ============================================================================= function showSettings() { var form = { // -------------------------------- "spacer1": { "type": "text", "label": "","just": "c", "bounds": { "x": 20, "w": 550, "h": 20 }, }, // -------------------------------- // Overwrite "overwrite_label" : { "type" : "text", "label" : "Overwrite", "just" : "l", "bounds" : { "x": 0, "w" : 140, }, }, "overwrite": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.overwrite }, // -------------------------------- "customFolder_label" : { "type" : "text", "label" : "Custom folder", "just" : "l", "bounds" : { "x": 0, "w" : 140, }, }, "customFolder": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.customFolder }, "customFolder_path" : { "type" : "fileselect", "path" : settings.customFolder_path, "editable" : false, "dir" : true, "saving" : true, "label" : "empty", "bounds" : { "y": -1, "x": 180, "w" : 400, }, "visible": settings.customFolder, }, // -------------------------------- "subFolder_label" : { "type" : "text", "label" : "Sub folder", "just" : "l", "bounds" : { "x": 0, "w" : 160, }, }, "subFolder": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.subFolder }, "subFolder_val": { "type": "texteditor", "bounds" : { "y": -1, "x" : 180, "w" : 400, }, "multiline" : false, "wordwrap" : false, "default" : settings.subFolder_val, "visible": settings.subFolder, }, // -------------------------------- "nametag_val_label" : { "type" : "text", "label" : "Name tag", "just" : "l", "bounds" : { "x": 0, "w" : 160, }, }, "nametag": { "type": "togglebox", "bounds" : { "y": -1, "x": 140, "w" : 25, }, "label" : "", "toggle" : settings.nametag }, "nametag_val": { "type": "texteditor", "bounds" : { "y": -1, "x" : 180, "w" : 400, }, "multiline" : false, "wordwrap" : false, "default" : settings.nametag_val, "visible": settings.nametag, }, // -------------------------------- "spacer3" : { "type" : "text", "label" : " ", "just" : "r", "bounds" : { "x": 0, "w" : 200, "h" : 20 }, }, // -------------------------------- "reset" : { "type" : "button", "label" : "reset", "bounds" : { "x": 330, "w" : 80 }, "returns" : -1 }, "cancel" : { "type" : "button", "label" : "cancel", "bounds" : { "y": -1, "x": 415, "w" : 80 }, "returns" : 0 }, "save" : { "type" : "button", "label" : "save", "bounds" : { "y": -1, "x": 500, "w" : 80 }, "returns" : 1 }, // -------------------------------- }; // -------------------------------- return dialog(header.title, "", "w", form); } // ============================================================================ // DIALOG CALLBACK function dialog_callback(props) { const directMapping = { overwrite: "toggle", customFolder: "toggle", nametag: "toggle", subFolder: "toggle", customFolder_path: "path", nametag_val: "text", subFolder_val: "text", }; // Dynamische toekenning if (directMapping[props.name]) { settings[props.name] = props[directMapping[props.name]]; } // ---------------- // UPDATE DIALOG dialog({ "customFolder_path" : { "visible" : settings['customFolder'] }, "subFolder_val" : { "visible" : settings['subFolder'] }, "nametag_val" : { "visible" : settings['nametag'] }, }); // ---------------- }