// ==UserScript==
// @name Instagram Media Pro - Open & Save Content
// @namespace https://greasyfork.org/en/users/1431907-theeeunknown
// @version 4.5
// @description Extract, open and save Instagram media with one click. Easily save photos and videos to Discord.
// @author TR0LL
// @match https://www.instagram.com/*
// @match *://*.cdninstagram.com/*
// @match *://*.fbcdn.net/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=instagram.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
discordWebhookUrl: '',
disableNewUrlFetchMethod: false,
prefetchAndAttachLink: false,
hoverToFetchAndAttachLink: true,
replaceJpegWithJpg: false,
postFilenameTemplate: '%id%-%datetime%-%medianame%',
storyFilenameTemplate: '%id%-%datetime%-%medianame%',
datetimeTemplate: '%y%%m%%d%_%H%%M%%S%',
checkInterval: 500 // ms
};
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand('Set Discord Webhook URL', promptForWebhookUrl);
}
function promptForWebhookUrl() {
const currentUrl = localStorage.getItem('instagramMediaExtractorWebhook') || CONFIG.discordWebhookUrl;
const newUrl = prompt('Enter your Discord webhook URL:', currentUrl);
if (newUrl !== null && newUrl.trim() !== '') {
localStorage.setItem('instagramMediaExtractorWebhook', newUrl.trim());
CONFIG.discordWebhookUrl = newUrl.trim();
alert('Discord webhook URL saved! It will be used for future notifications.');
}
}
document.addEventListener('keydown', function(event) {
if (event.altKey && (event.code === 'KeyW' || event.key === 'w')) {
promptForWebhookUrl();
}
});
const SVG = {
openNewTab: `<svg id="Capa_1" style="fill:%color;" viewBox="0 0 482.239 482.239" xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="m465.016 0h-344.456c-9.52 0-17.223 7.703-17.223 17.223v86.114h-86.114c-9.52 0-17.223 7.703-17.223 17.223v344.456c0 9.52 7.703 17.223 17.223 17.223h344.456c9.52 0 17.223-7.703 17.223-17.223v-86.114h86.114c9.52 0 17.223-7.703 17.223-17.223v-344.456c0-9.52-7.703-17.223-17.223-17.223zm-120.56 447.793h-310.01v-310.01h310.011v310.01zm103.337-103.337h-68.891v-223.896c0-9.52-7.703-17.223-17.223-17.223h-223.896v-68.891h310.011v310.01z"/></svg>`
};
const CACHE = {
infoCache: {},
mediaIdCache: {},
previousUrl: ""
};
const SELECTORS = {
savePostButton: 'article *:not(li)>*>*>*>div:not([class])>div[role="button"]:not([style]):not([tabindex="-1"])',
profileCircle: 'header section svg circle',
playSvgPath: 'path[d="M5.888 22.5a3.46 3.46 0 0 1-1.721-.46l-.003-.002a3.451 3.451 0 0 1-1.72-2.982V4.943a3.445 3.445 0 0 1 5.163-2.987l12.226 7.059a3.444 3.444 0 0 1-.001 5.967l-12.22 7.056a3.462 3.462 0 0 1-1.724.462Z"]',
pauseSvgPath: 'path[d="M15 1c-3.3 0-6 1.3-6 3v40c0 1.7 2.7 3 6 3s6-1.3 6-3V4c0-1.7-2.7-3-6-3zm18 0c-3.3 0-6 1.3-6 3v40c0 1.7 2.7 3 6 3s6-1.3 6-3V4c0-1.7-2.7-3-6-3z"]',
saveButtonPolygon: 'polygon[points="20 21 12 13.44 4 21 4 3 20 3 20 21"]',
saveButtonPath: 'path[d="M20 22a.999.999 0 0 1-.687-.273L12 14.815l-7.313 6.912A1 1 0 0 1 3 21V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1Z"]'
};
const PATTERNS = {
postId: /^\/p\/([^/]+)\//,
postUrl: /instagram\.com\/p\/[\w-]+\//,
storyUrl: /instagram\.com\/stories\/([\w.-]+)\/(\d+)/
};
const currentHostname = window.location.hostname;
if (currentHostname === 'www.instagram.com') {
document.addEventListener('keydown', handleKeyDown);
const checkExistTimer = setInterval(checkAndAddButtons, CONFIG.checkInterval);
function handleKeyDown(event) {
if (window.location.href === 'https://www.instagram.com/') return;
const mockEventTemplate = {
stopPropagation: function() {},
preventDefault: function() {}
};
if (event.altKey && (event.code === 'KeyI' || event.key === 'i')) {
const buttons = document.getElementsByClassName('open-btn');
if (buttons.length > 0) {
const mockEvent = { ...mockEventTemplate, currentTarget: buttons[buttons.length - 1] };
if (CONFIG.hoverToFetchAndAttachLink) handleMouseEnter(mockEvent);
handleButtonClick(mockEvent);
}
}
if (event.altKey && (event.code === 'KeyL' || event.key === 'l')) {
let btns = document.querySelectorAll('button[aria-label="Next"], button._afxw[aria-label="Next"]');
if (btns.length > 0) btns[btns.length-1].click();
else {
btns = document.getElementsByClassName('_9zm2');
if (btns.length > 0) btns[0].click();
}
}
if (event.altKey && (event.code === 'KeyJ' || event.key === 'j')) {
let btns = document.querySelectorAll('button[aria-label="Previous"], button._afxw[aria-label="Previous"]');
if (btns.length > 0) btns[btns.length-1].click();
else {
btns = document.getElementsByClassName('_9zm0');
if (btns.length > 0) btns[0].click();
}
}
}
function isPostPage() {
return Boolean(window.location.pathname.match(PATTERNS.postId));
}
function queryHas(root, selector, has) {
const nodes = root.querySelectorAll(selector);
for (let i = 0; i < nodes.length; ++i) {
if (nodes[i].querySelector(has)) return nodes[i];
}
return null;
}
function checkAndAddButtons() {
const curUrl = window.location.href;
const rgb = getComputedStyle(document.body).backgroundColor.match(/[.?\d]+/g);
const iconColor = (rgb?.length >= 3 && (parseInt(rgb[0])*0.299+parseInt(rgb[1])*0.587+parseInt(rgb[2])*0.114)<=150) ? 'white' : 'black';
if (CACHE.previousUrl !== curUrl) {
document.querySelectorAll('.custom-btn').forEach(btn => btn.remove());
CACHE.previousUrl = curUrl;
}
document.querySelectorAll('article:not(:has(.custom-btn))').forEach(article => {
const buttonAnchor = (Array.from(article.querySelectorAll(SELECTORS.savePostButton))).pop();
if (buttonAnchor) addCustomButton(buttonAnchor, iconColor, appendToPost);
});
if (isPostPage() && !document.querySelector('main > article .custom-btn')) {
const saveBtn =
queryHas(document, 'div[role="button"] > div[role="button"]:not([style])', SELECTORS.saveButtonPolygon) ||
queryHas(document, 'div[role="button"] > div[role="button"]:not([style])', SELECTORS.saveButtonPath);
if (saveBtn?.parentNode?.querySelector('svg')) {
addCustomButton(saveBtn.parentNode.querySelector('svg'), iconColor, appendToIndependentPost);
}
}
if (!document.querySelector('main > div > header .custom-btn') && !curUrl.includes("stor")) {
const profileAnchor = document.querySelector(SELECTORS.profileCircle);
if (profileAnchor) addCustomButton(profileAnchor, iconColor, appendToHeader);
}
if (!document.querySelector('section header .custom-btn') && curUrl.includes('/stories/')) {
const playPauseSvg =
queryHas(document, 'svg', SELECTORS.playSvgPath) ||
queryHas(document, 'svg', SELECTORS.pauseSvgPath);
if (playPauseSvg) addCustomButton(playPauseSvg.parentNode, 'white', appendToStory);
}
}
function appendToPost(node, btn) { node.append(btn); }
function appendToIndependentPost(node, btn) { node.parentNode.parentNode.append(btn); }
function appendToHeader(node, btn) { node.parentNode.parentNode.parentNode.appendChild(btn); }
function appendToStory(node, btn) {
if (node.parentNode.parentNode.parentNode.querySelector('.custom-btn')) {
return;
}
node.parentNode.parentNode.parentNode.append(btn);
}
function addCustomButton(node, iconColor, appendNodeFunc) {
const openBtn = createCustomButton(SVG.openNewTab, iconColor, 'open-btn', 'Open in new tab', '16px');
appendNodeFunc(node, openBtn);
if (CONFIG.prefetchAndAttachLink || CONFIG.hoverToFetchAndAttachLink) {
handleMouseEnter({ currentTarget: openBtn });
}
}
function createCustomButton(svg, iconColor, className, title, marginLeft) {
const newBtn = document.createElement('a');
newBtn.innerHTML = svg.replace('%color', iconColor);
newBtn.className = 'custom-btn ' + className;
newBtn.title = title;
newBtn.style.cssText = `cursor: pointer; margin-left: ${marginLeft}; margin-top: 8px; display: inline-flex; align-items: center; z-index: 999;`;
newBtn.onclick = handleButtonClick;
if (CONFIG.hoverToFetchAndAttachLink) {
newBtn.onmouseenter = handleMouseEnter;
}
newBtn.target = '_blank';
newBtn.rel = 'noopener noreferrer';
return newBtn;
}
function handleButtonClick(e) {
const target = e.currentTarget;
e.stopPropagation();
if (!target.getAttribute('href') || target.getAttribute('href').startsWith('blob:')) {
e.preventDefault();
if (window.location.pathname.includes('/stories/')) {
handleStoryClick(target);
} else if (document.querySelector('main > div > header')?.contains(target)) {
handleProfileClick(target);
} else {
handlePostClick(target);
}
}
}
async function handleMouseEnter(e) {
const target = e.currentTarget;
if (!CONFIG.hoverToFetchAndAttachLink) return;
if (target.getAttribute('href') && !target.getAttribute('href').startsWith('blob:')) return;
let url = null;
try {
if (window.location.pathname.includes('/stories/')) {
const node = findStorySection(target);
if (node) url = await getStoryUrl(target, node);
} else if (document.querySelector('main > div > header')?.contains(target)) {
url = getProfileUrl(target);
} else {
const node = findArticleNode(target);
if (node) {
const result = await getPostUrl(target, node);
url = result?.url;
}
}
} catch(err) {
}
if (url && !url.startsWith('blob:')) {
target.setAttribute('href', url);
} else {
target.removeAttribute('href');
}
}
async function handleProfileClick(target) {
const url = getProfileUrl(target);
if (url && !url.startsWith('blob:')) {
target.href = url;
}
}
async function handlePostClick(target) {
const articleNode = findArticleNode(target);
if (!articleNode) return;
try {
const result = await getPostUrl(target, articleNode);
const url = result?.url;
if (url && !url.startsWith('blob:')) {
target.href = url;
}
} catch(e) {
}
}
async function handleStoryClick(target) {
const sectionNode = findStorySection(target);
if (!sectionNode) return;
try {
const url = await getStoryUrl(target, sectionNode);
if (url && !url.startsWith('blob:')) {
target.href = url;
}
} catch(e) {
}
}
function getProfileUrl(target) {
const imgElem = document.querySelector('header img');
return imgElem ? imgElem.src : null;
}
function findArticleNode(target) {
let node = target;
while (node && node.tagName !== 'ARTICLE' && node.tagName !== 'MAIN') {
node = node.parentNode;
}
return node;
}
async function getPostUrl(target, articleNode) {
let list = articleNode.querySelectorAll('li[style][class]');
let url = null;
let mediaIndex = 0;
if (list.length === 0) {
if (!CONFIG.disableNewUrlFetchMethod) url = await getUrlFromInfoApi(articleNode);
if (url === null) {
let v = articleNode.querySelector('video');
if (v) {
url = v.getAttribute('src');
if (v.hasAttribute('videoURL')) url = v.getAttribute('videoURL');
else if (url === null || url.includes('blob')) url = await fetchVideoURL(articleNode, v);
} else if (articleNode.querySelector('article div[role] div > img')) {
url = articleNode.querySelector('article div[role] div > img').getAttribute('src');
}
}
} else {
const pV = location.pathname.startsWith('/p/');
let dE = [...articleNode.querySelectorAll(`div._acnb`)];
mediaIndex = dE.reduce((r, e, i) => (e.classList.length === 2 ? i : r), 0);
if (!CONFIG.disableNewUrlFetchMethod) url = await getUrlFromInfoApi(articleNode, mediaIndex);
if (url === null) {
const lE = [...articleNode.querySelectorAll(`:scope > div > div:nth-child(${pV ? 1 : 2}) > div > div:nth-child(1) ul li[style*="translateX"]`)];
const lW = Math.max(...lE.map(e => e.clientWidth));
const pM = lE.reduce((r, e) => {
const tM = e.style.transform.match(/-?(\d+)/);
if (tM?.[1]) {
const p = Math.round(Number(tM[1]) / lW);
return { ...r, [p]: e };
}
return r;
}, {});
const n = pM[mediaIndex];
if (n) {
if (n.querySelector('video')) {
let v = n.querySelector('video');
url = v.getAttribute('src');
if (v.hasAttribute('videoURL')) url = v.getAttribute('videoURL');
else if (url === null || url.includes('blob')) url = await fetchVideoURL(articleNode, v);
} else if (n.querySelector('img')) {
url = n.querySelector('img').getAttribute('src');
}
} else {
const fI = articleNode.querySelector('ul li img[src]');
const fV = articleNode.querySelector('ul li video');
if (fV) {
url = fV.getAttribute('src');
if (fV.hasAttribute('videoURL')) url = fV.getAttribute('videoURL');
else if (url === null || url.includes('blob')) url = await fetchVideoURL(articleNode, fV);
} else if (fI) {
url = fI.getAttribute('src');
}
}
}
}
return { url, mediaIndex };
}
function findHighlightsIndex() {
let c = document.querySelector('div[style^="transform"]')?.parentElement;
if (!c) return 0;
let p = c.parentElement;
if (!p) return 0;
let d = p.children;
return Array.from(d).indexOf(c);
}
async function getUrlFromInfoApi(articleNode, mediaIdx = 0) {
try {
const aP = /"X-IG-App-ID":"([\d]+)"/;
const mP = /instagram:\/\/media\?id=(\d+)|["' ]media_id["' ]:["' ](\d+)["' ]/;
function fA() {
let b = document.querySelectorAll("body > script");
for (let i = 0; i < b.length; ++i) {
let m = b[i].text.match(aP);
if (m) return m[1];
}
return null;
}
async function fM() {
function m1() {
let h = window.location.href;
let m = h.match(/www.instagram.com\/stories\/[^\/]+\/(\d+)/);
if (!h.includes('highlights') && m) return m[1];
}
async function m3() {
let p = await findPostId(articleNode);
if (!p) return null;
if (!(p in CACHE.mediaIdCache)) {
let u = `https://www.instagram.com/p/${p}/`;
let r = await fetch(u);
let t = await r.text();
let i = t ? t.match(mP) : null;
let m = null;
if (i) m = i.slice(1).find(id => id !== undefined) || null;
if (!m) return null;
CACHE.mediaIdCache[p] = m;
}
return CACHE.mediaIdCache[p];
}
function m2() {
let s = document.querySelectorAll('script[type="application/json"]');
for (let i = 0; i < s.length; i++) {
let m = s[i].text.match(/"pk":"(\d+)","id":"[\d_]+"/);
if (m) {
if (!window.location.href.includes('highlights')) return m[1];
let ms = Array.from(s[i].text.matchAll(/"pk":"(\d+)","id":"[\d_]+"/g), m => m[1]);
const x = findHighlightsIndex();
if (ms.length > x) return ms[x];
}
}
}
return m1() || await m3() || m2();
}
function gU(i) {
if ("video_versions" in i && i.video_versions.length > 0) return i.video_versions[0].url;
else if ("image_versions2" in i && i.image_versions2.candidates.length > 0) return i.image_versions2.candidates[0].url;
return null;
}
let a = fA();
if (!a) return null;
let h = {
method: 'GET',
headers: {
Accept: '*/*',
'X-IG-App-ID': a
},
credentials: 'include',
mode: 'cors'
};
let m = await fM();
if (!m) return null;
if (!(m in CACHE.infoCache)) {
let u = `https://i.instagram.com/api/v1/media/${m}/info/`;
let r = await fetch(u, h);
if (r.status !== 200) return null;
CACHE.infoCache[m] = await r.json();
}
let j = CACHE.infoCache[m];
if (!j?.items?.length) return null;
if ('carousel_media' in j.items[0]) {
if (j.items[0].carousel_media.length > mediaIdx) return gU(j.items[0].carousel_media[mediaIdx]);
else return null;
} else return gU(j.items[0]);
} catch (e) {
return null;
}
}
function findPostName(articleNode) {
let i = articleNode.querySelector('article section + * a[href^="/"][href$="/"]');
if (i) return i;
let a = articleNode.querySelector('canvas ~ * img');
if (a) {
a = a.getAttribute('alt');
let l = articleNode.querySelectorAll('a');
for (let i = 0; i < l.length; i++) {
const p = l[i].getAttribute('href').replace(/\//g, '');
if (a?.includes(p)) return l[i];
}
} else {
const e = document.querySelector('h2[dir]');
if (e) return e.innerText;
}
return null;
}
function findPostId(articleNode) {
let a = articleNode.querySelectorAll('a');
for (let i = 0; i < a.length; ++i) {
let l = a[i].getAttribute('href');
if (l) {
let m = l.match(PATTERNS.postId);
if (m) return m[1];
}
}
return null;
}
async function fetchVideoURL(articleNode, videoElem) {
let p = videoElem.getAttribute('poster');
if (!p) return null;
let t = articleNode.querySelectorAll('time');
if (t.length === 0) return null;
let l = t[t.length - 1].parentNode?.parentNode;
if (!l?.href) return null;
let pU = l.href;
const pP = /\/([^\/?]*)\?/;
let pM = p.match(pP);
if (!pM?.[1]) return null;
let fN = pM[1];
try {
let r = await fetch(pU);
if (!r.ok) throw new Error();
let c = await r.text();
const pt = new RegExp(`"${fN}".*?video_versions.*?url":"([^"]*)"`, 's');
let m = c.match(pt);
if (!m?.[1]) {
const aP = new RegExp(`"video_url":"([^"]+)"`);
m = c.match(aP);
if (!m?.[1]) return null;
}
let vU = JSON.parse(`"${m[1].replace(/\\u([\da-f]{4})/gi, (m, g) => String.fromCharCode(parseInt(g, 16)))}"`.replace(/\\"/g, '"'));
videoElem.setAttribute('videoURL', vU);
return vU;
} catch (e) {
return null;
}
}
function findStorySection(target) {
let node = target;
while (node && node.tagName !== 'SECTION') {
node = node.parentNode;
}
return node;
}
async function getStoryUrl(target, sectionNode) {
let url = null;
if (!CONFIG.disableNewUrlFetchMethod) {
url = await getUrlFromInfoApi(sectionNode);
}
if (!url) {
if (sectionNode.querySelector('video > source')) {
url = sectionNode.querySelector('video > source').getAttribute('src');
} else if (sectionNode.querySelector('img[decoding="sync"]')) {
let img = sectionNode.querySelector('img[decoding="sync"]');
let srcset = img.getAttribute('srcset');
if (srcset) {
url = srcset.split(/ \d+w/g)[0].trim();
}
if (!url || url.length === 0) {
url = img.getAttribute('src');
}
} else if (sectionNode.querySelector('video')) {
url = sectionNode.querySelector('video').getAttribute('src');
}
}
return url;
}
} else if (currentHostname.endsWith('.cdninstagram.com') || currentHostname.endsWith('.fbcdn.net')) {
async function sendToDiscord(imageUrl, actionType, filename = 'media') {
if (!imageUrl || typeof imageUrl !== 'string' || imageUrl.startsWith('blob:')) return;
if (!CONFIG.discordWebhookUrl || CONFIG.discordWebhookUrl.trim() === '') {
promptForWebhookUrl();
if (!CONFIG.discordWebhookUrl || CONFIG.discordWebhookUrl.trim() === '') {
return;
}
}
try {
const isVideo = /\.mp4|\/video\/|_n\.mp4/i.test(imageUrl);
const isImage = /\.jpg|\.jpeg|\.png|\.webp|_n\.jpg|_n\.png/i.test(imageUrl);
let simpleFilename = filename.split('?')[0].replace(/[^a-zA-Z0-9_\-.]/g, '_');
const fileExt = isVideo ? '.mp4' : '.jpg';
if (!simpleFilename.endsWith(fileExt)) {
simpleFilename += fileExt;
}
const response = await fetch(imageUrl);
if (!response.ok) throw new Error('Failed to fetch media');
const blob = await response.blob();
const formData = new FormData();
formData.append('content', `Instagram Media ${actionType}`);
const fileType = isVideo ? 'video/mp4' : 'image/jpeg';
const file = new File([blob], simpleFilename, { type: fileType });
formData.append('file', file);
const webhookResponse = await fetch(CONFIG.discordWebhookUrl, {
method: 'POST',
body: formData
});
if (!webhookResponse.ok) throw new Error('Discord webhook failed');
} catch (error) {
try {
const payload = {
content: `Instagram Media ${actionType}`,
embeds: [{
url: imageUrl,
image: {
url: imageUrl
}
}]
};
await fetch(CONFIG.discordWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (e) {
try {
const textPayload = {
content: `Instagram Media ${actionType}: ${imageUrl}`
};
await fetch(CONFIG.discordWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(textPayload)
});
} catch (finalError) {
}
}
}
}
const currentMediaUrl = window.location.href;
if (currentMediaUrl && /\.(jpg|jpeg|png|webp|mp4|mov)(\?|$)/i.test(currentMediaUrl)) {
let filename = 'media_file';
try {
const urlPath = new URL(currentMediaUrl).pathname;
const lastPart = urlPath.substring(urlPath.lastIndexOf('/') + 1);
const potentialFilename = lastPart.split('?')[0];
if (potentialFilename && potentialFilename.length > 1) {
filename = potentialFilename;
}
} catch (e) {
}
sendToDiscord(currentMediaUrl, 'Opened Direct', filename);
}
}
})();