Copy and paste from google sheets?
Is there an efficient way to copy and paste from a column in google sheets to clips in pro-tools? All clips will be in order from left to right corresponding to To to Bottom in excel. The only hitch is that periodically there will be an empty cell, so the script would have to know to ignore those. But it's all in one column and on one track. Top to bottom, left to right. That part is pretty straight forward. Just not sure how well this well function in sound flow with the UI picker, I've had issues with it. A buddy does this in Keyboard maestro, but it seems silly to buy that just for the one thing.
Linked from:
- Chad Wahlbrink @Chad2023-10-27 13:23:58.452Z
Are you trying to copy from a web browser, or would you download the Google Sheet as an XLSX file to open in Excel or Apple Numbers?
- In reply toDane_Butler⬆:Chad Wahlbrink @Chad2023-10-27 15:19:15.043Z
@Dane_Butler,
Here's a script that may work for you! If you can download a CSV file from Google Sheets, you'll be good to go. You can do this from Google Sheets file menu ("File" > "Download" > "Comma Separated Values (.csv)").
Select the first clip on the timeline in Pro Tools and then run this script. The script will prompt you for a file path and a column name. Then it will go through each clip on the timeline adding the file name. By default, I'm appending the index of the cell to the beginning of the clip name to avoid duplicate names, but you could remove that by settingconst appendIndex = false;
in line 5
Let me know if this works for you.
/* DEFINE VARIABLES */ const filePath = sf.interaction.selectFile({prompt:"Select a CSV File"}).path; const targetColumn = sf.interaction.popupText({title:"What Column?"}).text; const appendIndex = true; /* THE CODE */ // Rename Clips // Define Source Array and whether to append index number // Append Index can help remove possibility of duplicate names. function renameClips(source, appendIndex) { //Make sure we have no search in Clips list - if we had, Clip Rename won't work (Pro Tools bug) sf.keyboard.press({ keys: 'cmd+shift+d' }); //Open Rename window sf.ui.proTools.menuClick({ menuPath: ['Clip', 'Rename...'] }); //Reference for Rename Window const win = sf.ui.proTools.windows.whoseTitle.is('Name'); //Wait for rename Window win.first.elementWaitFor(); //Set the clip name to Track name win.first.groups.first.textFields.first.elementSetTextFieldWithAreaValue({ value: appendIndex ? `${i}_` + source[i] : source[i], }); // Click OK sf.ui.proTools.windows.whoseTitle.is("Name").first.buttons.whoseTitle.is("OK").first.elementClick(); // Wait for Window to leave sf.ui.proTools.windows.whoseTitle.is("Name").first.elementWaitFor({ waitType: "Disappear" }); } //Convert CSV to Text let csvAsText = sf.file.readText({ path: filePath }).text // Extract each rows let rows = csvAsText.split(/(?:\r\n|\n)+/).filter(function(el) {return el.length != 0}); // Extract the headers let headers = rows.splice(0, 1)[0].split(","); // Complex gobbledegook to filter the CSV let valuesRegExp = /(?:\"([^\"]*(?:\"\"[^\"]*)*)\")|([^\",]+)/g; // Create an array to hold our rows with key value pairs let cells = []; // Add our row objects to the array with keys for (let i = 0; i < rows.length; i++) { let element = {}; let j = 0; var matches; while (matches = valuesRegExp.exec(rows[i])) { var value = matches[1] || matches[2]; value = value.replace(/\"\"/g, "\""); element[headers[j]] = value; j++; } cells.push(element); } // Let's narrow our focus to one column let filteredColumn = cells.filter(x => x[targetColumn]); // Let's get just the content in the cells of this column let filteredColumnContents = filteredColumn.splice(0, filteredColumn.length).map(x => x[targetColumn]); sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); let i = 0; // Do for each selected clip while (i < filteredColumnContents.length) { // Rename our Clips renameClips(filteredColumnContents, appendIndex) // Go to Next Clip sf.keyboard.press({keys:"control+tab"}); // Increment i i++; }
- DDane Butler @Dane_Butler
Legend! I'll try this out here in a bit. Thank you!
- In reply toChad⬆:DDane Butler @Dane_Butler
I followed your steps, and I get to the part where it asks me to select a column but then I get the following error....
27.10.2023 14:05:44.34 [Backend]: JavaScript error with InnerException: null
!! Command Error: CSV_DATA [user:cl8gl08n50000s710g0g56h2t:clo8rp98n0000lo10l0gzm4ft]:
TypeError: Cannot read property 'replace' of undefined
(CSV_DATA line 60)Chad Wahlbrink @Chad2023-10-27 19:19:19.080Z
Interesting, @Dane_Butler.
Could I have you try adding those parameters explicitly to test. It would mean swapping the first couple lines of the script with something like this./* DEFINE VARIABLES */ const filePath = "~/Downloads/TestDoc.csv" const targetColumn = "ColumnName"; const appendIndex = true;
See if that can help you test it out. To get a file path from finder you can use ⌥⌘C to copy the path to your clipboard.
- DDane Butler @Dane_Butler
For "ColumnName" would I literally type "K" in my case?
- DDane Butler @Dane_Butler
Yeah that instantly errors out in the same way!
Chad Wahlbrink @Chad2023-10-27 19:38:36.907Z
It'll be hard for me to test without a more defined example of a CSV you are working from. Would you be able to share a "dummy" example CSV file? Feel free to send it to support@soundflow.org as a zip file if you don't want to link to one publicly.
- DDane Butler @Dane_Butler
Sent, yes I deleted al the data but one column, with redactions but still keep it secure and delete after diagnostics. this data was stored in K, but after editing the CSV it moved it to column A when I tried to open it in excel
Chad Wahlbrink @Chad2023-10-27 19:47:36.747Z
Thanks, @Dane_Butler,
If you don't mind, can you resend and make sure to send it as a zip file. CSV's don't attach correctly to our support email, but zip files will work. (right click and compress it, etc). We will surely keep it secure and delete after testing.
Thanks for helping us troubleshoot the workflow!- DDane Butler @Dane_Butler
Resent! I reverted to the original script, and now it errors out on the OG CSV, and the edited CSV I sent you it does nothing. Sometimes I run into inconsistencies with SF that make it hard to diagnose problems.
Chad Wahlbrink @Chad2023-10-27 20:46:51.159Z2023-11-01 18:03:29.174Z
Thanks, @Dane_Butler!
I now understand we were working in slightly different modalities. I was assuming the first row of a CSV would be a row of "column name headers," - so when I was referring to "Column Name," I was referring to the name of that header in Row 1 ("Name," "Scene," etc.). However, I think you thought it was referring to the column's letter, which ALSO makes a lot of sense.
Anyway, here's a new version of the script to try out. I'll also shoot you a quick email reply with a little demo video of how it is working on my system.
/* DEFINE VARIABLES */ const filePath = sf.interaction.selectFile({ prompt: "Select a CSV File" }).path; const targetColumn = sf.interaction.popupText({ title: "What Column?" }).text.toUpperCase(); const appendIndex = true; const nameClipOnly = true; let lookUp = { "A": 0, "B": 1, "C": 2, "D": 3, "E": 4, "F": 5, "G": 6, "H": 7, "I": 8, "J": 9, "K": 10, "L": 11, "M": 12, "N": 13, "O": 14, "P": 15, "Q": 16, "R": 17, "S": 18, "T": 19, "U": 20, "V": 21, "W": 22, "X": 23, "Y": 24, "Z": 25, } /* THE CODE */ // Rename Clips // Define Source Array and whether to append index number // Append Index can help remove possibility of duplicate names. function renameClips(source, appendIndex) { //Make sure we have no search in Clips list - if we had, Clip Rename won't work (Pro Tools bug) sf.keyboard.press({ keys: 'cmd+shift+d' }); //Open Rename window sf.ui.proTools.menuClick({ menuPath: ['Clip', 'Rename...'] }); //Reference for Rename Window const win = sf.ui.proTools.windows.whoseTitle.is('Name'); //Wait for rename Window win.first.elementWaitFor(); //Set the clip name to Track name win.first.groups.first.textFields.first.elementSetTextFieldWithAreaValue({ value: appendIndex ? `${i}_` + source[i] : source[i], }); if(nameClipOnly && sf.ui.proTools.windows.whoseTitle.is("Name").first.groups.whoseTitle.is("Name").first.radioButtons.whoseTitle.is("name clip only").first.exists){ sf.ui.proTools.windows.whoseTitle.is("Name").first.groups.whoseTitle.is("Name").first.radioButtons.whoseTitle.is("name clip only").first.elementClick(); } // Click OK sf.ui.proTools.windows.whoseTitle.is("Name").first.buttons.whoseTitle.is("OK").first.elementClick(); // Wait for Window to leave sf.ui.proTools.windows.whoseTitle.is("Name").first.elementWaitFor({ waitType: "Disappear" }); } //Convert CSV to Text let csvAsText = sf.file.readText({ path: filePath }).text // Extract each rows let rows = csvAsText.split(/(?:\r\n|\n)+/).filter(function (el) { return el.length != 0 }); // Extract the headers // let headers = rows.splice(0, 1)[0].split(","); // Complex gobbledegook to filter the CSV let valuesRegExp = /(?:\"([^\"]*(?:\"\"[^\"]*)*)\")|([^\",]+)/g; // Create an array to hold our rows with key value pairs let cells = []; // Add our row objects to the array with keys for (let i = 0; i < rows.length; i++) { let element = {}; let j = 0; var matches; while (matches = valuesRegExp.exec(rows[i])) { var value = matches[1] || matches[2]; value = value.replace(/\"\"/g, "\""); if (j == lookUp[targetColumn]) { element[lookUp[targetColumn]] = value; } j++; } cells.push(element); } // Let's narrow our focus to one column let filteredColumn = cells.filter(x => x[lookUp[targetColumn]]); // Let's get just the content in the cells of this column let filteredColumnContents = filteredColumn.map(x => x[lookUp[targetColumn]]); sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); let i = 0; // Do for each selected clip while (i < filteredColumnContents.length) { // Rename our Clips renameClips(filteredColumnContents, appendIndex) // Go to Next Clip sf.keyboard.press({ keys: "control+tab" }); // Increment i i++; }
- DDane Butler @Dane_Butler
Just reading through the code while I'm on the go! This makes so much more sense! I'm still a coding noob, and I basically just patch together stuff I find online or GPT helps me generate, so I really appreciate this a lot. I'll let you know how it lands once I get back to my desktop!
- DDane Butler @Dane_Butler
I don't know why but I'm still getting the same error.
- In reply toChad⬆:DDane Butler @Dane_Butler
27.10.2023 19:47:42.98 [Backend]: >> Command: CSV_DATA [user:cl8gl08n50000s710g0g56h2t:clo8rp98n0000lo10l0gzm4ft]
27.10.2023 19:47:47.54 [Backend]: JavaScript error with InnerException: null
27.10.2023 19:47:47.54 [Backend]: #App: Activate "com.avid.ProTools" -> Show Pro Tools Deck [cl5r6g8yo0001c710x6rnzup9]
27.10.2023 19:47:47.55 [Backend]: !! Command Error: CSV_DATA [user:cl8gl08n50000s710g0g56h2t:clo8rp98n0000lo10l0gzm4ft]:
TypeError: Cannot read property 'replace' of undefined
(CSV_DATA line 60)Chad Wahlbrink @Chad2023-10-28 04:29:44.537Z
Hmm, that's interesting!
It could be a permissions issue, similar to this post.
Can I get you to check these permissions settings in System Preferences > Security & Privacy > Files and Folders > SoundFlow
If there are "unchecked" boxes here, then check them, OR delete all of them using the "-" button, and try running the scripts again.
You should be prompted with something like this:- DDane Butler @Dane_Butler
That's a good idea! That would make a lot of sense actually. I'll give it a go and update you! I'm excited to get this working because it will really help me with my job.
- In reply toChad⬆:DDane Butler @Dane_Butler
Ok! So we finally have lift off, and it seems to be working great! Thank you so much for your help. I was going through the code that you wrote, and there was this code on line 77 let valuesRegExp = /(?:"([^"](?:""[^"])*)")|([^",]+)/g; that made me realize I definitely would not have been able to figure this out on my own with my limited knowledge of web development and game engine code.
What was the fix? So first I tried to go and remove / re add soundflow to my files preferences and that actually didn't change anything. What did change, is that I deleted the script file and started a new one from scratch. Before hand I was trying to update the code within the script, or save and utilize a script version, but I guess despite the new code being visible in the UI when I quite and reloaded the program, the new code was NOT being honored on the backend behind the scenes, and only by deleting the script and creating a new one, then dropping in your updated code did it work.
I love soundflow, it's honestly a miracle program for sound editors, but I think I can speak for many of the users on this platform when I say we we would be completely sunk without developers like yourself or Samuel. Thank you so much Chad!
- DDane Butler @Dane_Butler
Also! This might be a big ask so no worries if this is an issue, but would a backup script be available that does exactly what this one does, but doesn't change the file name, and instead only effects the clip name? This might sound odd, but I actually have use for both types depending on the session type I'm working on. At the end of the day I'm exporting rendered regions not bouncing so it ends up working the same on the deliverables side, but every once in a while I'll discover an inconsistency in the script that will throw a monkey wrench in the system, and having the file names changed can make fixing those problems a chore sometimes.
Chad Wahlbrink @Chad2023-10-30 14:34:04.185Z
Ah, yes! I'll look into that this week.
I'm so happy we got it working in some capacity, though! Happy to help.
Also, yes, this line is scary:let valuesRegExp = /(?:"([^"](?:""[^"])*)")|([^",]+)/g;
I honestly haven't fully dismantled what that is doing, but I grabbed it from another thread online to parse CSV files with Javascript. It seemed to filter the CSV text file to what I needed, so I ran with it. Regex is really powerful, but can be complex. If you are ever interested, check out this site: https://regex101.com/
- DDane Butler @Dane_Butler
Hey Chad! Thank you for that link. Idk why but it's not working again sadly. I'm wondering if it's just stability issues with soundflow. I have a script that Sam wrote for me that works most days, and then suddenly it will just stop working, or fail a bunch and then work. Frustrating, idk what to do.
- DDane Butler @Dane_Butler
It's the replace line every time.
TypeError: Cannot read property 'replace' of undefined
(CSV_REV2 line 90)Chad Wahlbrink @Chad2023-11-01 01:08:42.511Z
Interesting, I haven’t had time to dig back into this one, but I’ll try to give it another go later this week.
I may be able to engineer a different method of parsing the CSV file that could be more consistent. There may be some specific text characters in the CSV you are working from that is causing an issue.- DDane Butler @Dane_Butler
THAT's IT!!!!! YOU'RE A GENIUS!!!!! The japanese characters! The computer doesn't know how to process them! That's what's braking the code! IN the original I gave you I redacted everything, including all special characters, japanese characters, or any other kind of character that wasn't an english word or number!!! It's working now!
Chad Wahlbrink @Chad2023-11-01 18:01:28.733Z2023-11-01 18:08:08.470Z
I'm glad that worked, @Dane_Butler!
Here's another version of the script that will allow you to select whether to "name clip only" or "name clip and disk file."
to name clip only leave this set to true:const nameClipOnly = true;
to name clip and disk file, set it to false:
const nameClipOnly = false;
I added the same parameter for "name clip only" to the original script here: Copy and paste from google sheets? #post-13. Note - I did find some scenarios where this first version wasn't working as expected. Specifically, if a CSV file has some empty cells before the desired column, the values could be parsed incorrectly with that first script.
The script below is a slight rewrite with less regex going on, but it handles the empty cells more intelligently. The only caveat here is that you should be careful not to have a "comma" character in any cell you are reading from. A CSV is a "Comma-Separated-Value" document, so additional comma characters can become trickier to parse. If you do have cells with commas, I'd suggest removing those from the export you reference for this specific workflow.
/* DEFINE VARIABLES */ const filePath = sf.interaction.selectFile({ prompt: "Select a CSV File" }).path; const targetColumn = sf.interaction.popupText({ title: "What Column?" }).text.toUpperCase(); const appendIndex = true; const nameClipOnly = true; let lookUp = { "A": 0, "B": 1, "C": 2, "D": 3, "E": 4, "F": 5, "G": 6, "H": 7, "I": 8, "J": 9, "K": 10, "L": 11, "M": 12, "N": 13, "O": 14, "P": 15, "Q": 16, "R": 17, "S": 18, "T": 19, "U": 20, "V": 21, "W": 22, "X": 23, "Y": 24, "Z": 25, } /* THE CODE */ // Rename Clips // Define Source Array and whether to append index number // Append Index can help remove possibility of duplicate names. function renameClips(source, appendIndex) { //Make sure we have no search in Clips list - if we had, Clip Rename won't work (Pro Tools bug) sf.keyboard.press({ keys: 'cmd+shift+d' }); //Open Rename window sf.ui.proTools.menuClick({ menuPath: ['Clip', 'Rename...'] }); //Reference for Rename Window const win = sf.ui.proTools.windows.whoseTitle.is('Name'); //Wait for rename Window win.first.elementWaitFor(); //Set the clip name to Track name win.first.groups.first.textFields.first.elementSetTextFieldWithAreaValue({ value: appendIndex ? `${i}_` + source[i] : source[i], }); if(nameClipOnly && sf.ui.proTools.windows.whoseTitle.is("Name").first.groups.whoseTitle.is("Name").first.radioButtons.whoseTitle.is("name clip only").first.exists){ sf.ui.proTools.windows.whoseTitle.is("Name").first.groups.whoseTitle.is("Name").first.radioButtons.whoseTitle.is("name clip only").first.elementClick(); } // Click OK sf.ui.proTools.windows.whoseTitle.is("Name").first.buttons.whoseTitle.is("OK").first.elementClick(); // Wait for Window to leave sf.ui.proTools.windows.whoseTitle.is("Name").first.elementWaitFor({ waitType: "Disappear" }); } //Convert CSV to Text let csvAsText = sf.file.readText({ path: filePath }).text // Extract each rows let rows = csvAsText.split(/(?:\r\n|\n)+/).filter(function (el) { return el.length != 0 }); // Create an array to hold our rows with key value pairs let cells = []; // Add our row objects to the array with keys for (let i = 0; i < rows.length; i++) { let value = rows[i].split(',')[lookUp[targetColumn]]; value = value.replace(/[\.\*\|\"\:\<\>\?\/\\]/g, ''); cells.push(value); } // Filter empty cells cells = cells.filter(x => x != ""); // Let's get just the content in the cells of this column sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.mainWindow.invalidate(); let i = 0; // Do for each selected clip while (i < cells.length) { // Rename our Clips renameClips(cells, appendIndex) // Go to Next Clip sf.keyboard.press({ keys: "control+tab" }); // Increment i i++; }
- DDane Butler @Dane_Butler
Rockstar Chad, Thank you I will add this to the banks and give it a run when the next batch of VO files comes through. I appreciate your helpfulness on this issue. Nothing hurts me more than manually copy and pasting from a spreadsheet!