Action
Save Instagram Posts
Posted by garbonsai,
Last update
25 days ago
UPDATES
25 days ago
Bugfix for posts that have been removed (soft 404s).
25 days ago
Bugfix for posts that have been removed (soft 404s).
26 days ago
Added support for reels.
26 days ago
Change keyboard shortcut to avoid conflict.
26 days ago
This is a complete rewrite due to Meta turning off the JSON endpoint for Instagram posts. Unfortunately, comments can no longer be retrieved and I’m still working on TV and reels support.
over 1 year ago
- added option to change draft title prefix
- added option to remove tags from newly created draft(s)
- added option to delete source draft on success
- made error handling more robust
- switched to built in error, info, success, and warning messages
- cleaned up and refactored code
Searches draft for any Instagram post or reel URLs and saves the contents of each as a new draft. Does not work for Private Accounts. Edit the template tags to modify the templates and settings.
Steps
-
defineTemplateTag
name imageTemplate
template 
-
defineTemplateTag
name videoTemplate
template -
defineTemplateTag
name postTemplate
template [[draftTitle]] [[postCaption]] [[postMedia]] [[[postUser]]](https://www.instagram.com/[[postUser]]), [[[postDate]]]([[postUrl]])
-
defineTemplateTag
name titlePrefix
template #
-
defineTemplateTag
name captionPrefix
template -
defineTemplateTag
name mediaGlue
template -
defineTemplateTag
name dateFormat
template %a, %d %b %Y %T %z
-
defineTemplateTag
name escapeHashes
template false
-
defineTemplateTag
name reuseTitle
template false
-
defineTemplateTag
name copyTags
template true
-
defineTemplateTag
name tagsToAdd
template @instagram
-
defineTemplateTag
name tagsToRemove
template instagram_old
-
defineTemplateTag
name archiveNewDrafts
template true
-
defineTemplateTag
name flagNewDrafts
template true
-
defineTemplateTag
name loadNewDrafts
template true
-
script
// ******************** changelog ******************** // // v3.0.3: bug fix for posts that have been removed // v3.0.2: added support for reels // v3.0.1: changed keyboard shortcut // v3.0: // - complete rewrite because meta turned off JSON endpoint - thanks meta :( // - now uses the JSON-LD embedded in each post's HTML on Instagram // - now uses template tags for settings and templates // - now honors action's 'After Success' setting // - remove ability to retrieve comments - thanks meta :( // v2.0.2: fix for 'Script Error: ReferenceError: Can't find variable: requestUrl' // v2.0.1: added option to delete source draft on warning and error // v2.0: // - added option to change draft title prefix // - added option to remove tags from newly created draft(s) // - added option to delete source draft on success // - made error handling more robust // - switched to built in error, info, success, and warning messages // - cleaned up and refactored code // v1.9.5: added support to archive newly created draft // v1.9.4: jump to top of draft if loaded // v1.9.3: code cleanup // v1.9.2: added support to flag newly created draft // v1.9.1: added support for reels and tv // v1.9: tweaked default template // v1.8: updated to allow for running against multiple drafts at once // v1.7: added ability to reuse title of current draft for new draft(s) // v1.6: new default template to better accommodate searching Drafts // v1.5: gracefully handle non-fatal errors // v1.4: added ability to optionally both tag and load newly created drafts // v1.3: fix for changes to how paged threaded comments are queried // v1.2: code cleanup and refactoring // v1.1: now gets all comments, not just most recent // v1.0: initial release // ******************** templates ******************** // // default video template const defVideoTemplate = '<video controls height="[[postVideoHeight]]" width="[[postVideoWidth]]" ' + 'style="height:auto;max-width:[[postVideoWidth]]px;width:100%"><source ' + 'src="[[postVideoUrl]]" type="video/mp4"></video>'; // default post template const defPostTemplate = `[[draftTitle]] [[postCaption]] [[postMedia]] [[[postUser]]](https://www.instagram.com/[[postUser]]), [[[postDate]]]([[postUrl]])`; // ******************** functions ******************** // // alert the user and handle breaking errors function alertUser(type, message, log) { switch (type) { case 'error': app.displayErrorMessage(message); throw log; break; case 'success': app.displaySuccessMessage(message); break; default: app.displayInfoMessage(message); } } // convert 'true' and 'false' to boolean or return a default function strToBool(string, otherwise = false) { return string === 'true' || string === 'false' ? JSON.parse(string) : otherwise; } // check if a template tag exists and if not, return a default function ttExists(tag, otherwise) { return tag !== '' ? tag : otherwise; } // verify a string is valid JSON function verifyJson(string) { let result = JSON.parse(string); if (!result || typeof result !== 'object') { alertUser( 'error', 'JSON-LD is not valid.', `Invalid JSON-LD: ${string}` ); } return result; } // ******************** main ******************** // // set various options using template tags or defaults const imageTemplate = ttExists( draft.getTemplateTag('imageTemplate'), '' ); const videoTemplate = ttExists( draft.getTemplateTag('videoTemplate'), defVideoTemplate ); const postTemplate = ttExists( draft.getTemplateTag('postTemplate'), defPostTemplate ); const titlePrefix = ttExists(draft.getTemplateTag('titlePrefix'), '# '); const captionPrefix = draft.getTemplateTag('captionPrefix'); const mediaGlue = draft.getTemplateTag('mediaGlue'); const dateFormat = ttExists( draft.getTemplateTag('dateFormat'), '%a, %d %b %Y %T %z' ); const escapeHashes = strToBool(draft.getTemplateTag('escapeHashes')); const reuseTitle = strToBool(draft.getTemplateTag('reuseTitle')); const copyTags = strToBool(draft.getTemplateTag('copyTags', true)); const tagsToAdd = draft.getTemplateTag('tagsToAdd').split(','); const tagsToRemove = draft.getTemplateTag('tagsToRemove').split(','); const archiveNewDrafts = strToBool(draft.getTemplateTag('archiveNewDrafts')); const flagNewDrafts = strToBool(draft.getTemplateTag('flagNewDrafts')); const loadNewDrafts = strToBool(draft.getTemplateTag('loadNewDrafts', true)); // find any Instagram post URLs in the draft try { let postUrls = []; let postUrlRegExp = new RegExp( /(?:https?:\/\/)?(?:w{3}\.)?instagram\.com\/(p|reel)\/[a-zA-Z0-9_\-]+/, 'gi' ); let postUrlMatch; while ((postUrlMatch = postUrlRegExp.exec(draft.content)) !== null) { postUrls.push([postUrlMatch[0], postUrlMatch[1]]); } if (postUrls.length === 0) { alertUser('error', 'No post URLs found.', 'No post URLs found.'); } // retrieve each post URL that was found postUrls.forEach(function (postUrl) { let http = HTTP.create(); let httpRequest = http.request({ method: 'GET', url: postUrl[0], }); if (!httpRequest.success) { alertUser( 'error', 'Unable to retrieve post.', `Unable to retrieve ${postUrl[0]}.` ); } // extract the JSON-LD post data let jsonRegExp = new RegExp( /<script type="application\/ld\+json" nonce=".*?">(.*?)<\/script>/, 'i' ); let jsonData = jsonRegExp.exec(httpRequest.responseText); if (jsonData === null) { alertUser( 'error', 'No JSON-LD found.', `No JSON-LD found at ${postUrl[0]}.` ); } let postData = verifyJson(jsonData[1]); // format the post media let postMedia = []; if (postUrl[1] === 'p') { postData.image.forEach(function (postImage) { draft.setTemplateTag('postImage', postImage.url); postMedia.push(draft.processTemplate(imageTemplate)); }); } else if (postUrl[1] === 'reel') { postData.video.forEach(function (postVideo) { draft.setTemplateTag('postVideoUrl', postVideo.contentUrl); draft.setTemplateTag('postVideoHeight', postVideo.height); draft.setTemplateTag('postVideoWidth', postVideo.width); postMedia.push(draft.processTemplate(videoTemplate)); }); } // format the post caption let postCaption = postData.articleBody; if (escapeHashes) postCaption = postCaption.replace(/^#/gm, '\\#'); postCaption = postCaption.replace(/^/gm, captionPrefix); // format the post date let postDate = strftime(new Date(postData.dateCreated), dateFormat); // set the necessary template tags draft.setTemplateTag('postUrl', postUrl[0]); draft.setTemplateTag('postMedia', postMedia.join(mediaGlue)); draft.setTemplateTag('postCaption', postCaption); draft.setTemplateTag('postUser', postData.author.identifier.value); draft.setTemplateTag('postDate', postDate); reuseTitle ? draft.setTemplateTag('draftTitle', draft.title) : draft.setTemplateTag( 'draftTitle', titlePrefix + postData.identifier.value ); // create the new draft let d = Draft.create(); d.content = draft.processTemplate(postTemplate); if (copyTags) draft.tags.forEach((tag) => d.addTag(tag)); tagsToAdd.forEach((tag) => d.addTag(tag)); tagsToRemove.forEach((tag) => d.removeTag(tag)); d.isFlagged = flagNewDrafts; d.isArchived = archiveNewDrafts; d.update(); // load the new draft if (loadNewDrafts) { editor.load(d); editor.setSelectedRange(0, 0); } }); // display a success notification alertUser('success', 'Post successfully saved.', ''); // log thrown errors } catch (e) { console.log(e); }
Options
-
After Success Trash Notification Error Log Level Error
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.