您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Extract, open and save Instagram media with one click. Easily save photos and videos to Discord.
// ==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); } } })();