您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Open the original source of an IG post, story or profile picture. No jQuery
当前为
// ==UserScript== // @name Instagram Source Opener // @version 0.7.1 // @description Open the original source of an IG post, story or profile picture. No jQuery // @author jomifepe // @icon https://www.instagram.com/favicon.ico // @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js // @include https://www.instagram.com/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @namespace https://gf.qytechs.cn/users/192987 // ==/UserScript== (function() { "use strict"; const LOGGING_ENABLED = true; /* this script relies a lot on class names, I'll keep an eye on changes */ const IG_C_STORY_CONTAINER = "yS4wN"; const IG_C_STORY_MEDIA_CONTAINER = "qbCDp"; const IG_C_POST_IMG = "FFVAD"; const IG_C_POST_VIDEO = "tWeCl"; const IG_C_SINGLE_POST_CONTAINER = "JyscU"; const IG_C_MULTI_POST_SCROLLER = "MreMs"; const IG_C_MULTI_POST_LIST_ITEM = "_-1_m6"; const IG_C_POST_CONTAINER = "_8Rm4L"; const IG_S_POST_BUTTONS = ".eo2As > section"; const IG_C_PROFILE_PIC_CONTAINER = "RR-M-"; const IG_C_PRIVATE_PROFILE_PIC_CONTAINER = "M-jxE"; const IG_C_PRIVATE_PIC_IMG_CONTAINER = "_2dbep"; const IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER = "IalUJ"; const IG_C_PROFILE_CONTAINER = "v9tJq"; const IG_C_PROFILE_USERNAME_TITLE = "fKFbl"; const C_BTN_STORY = "iso-story-btn"; const C_BTN_STORY_CONTAINER = "iso-story-container"; const C_POST_WITH_BUTTON = "iso-post"; const C_BTN_POST_OUTER_SPAN = "iso-post-container"; const C_BTN_POST = "iso-post-btn"; const C_BTN_POST_INNER_SPAN = "iso-post-span"; const C_BTN_PROFILE_PIC_CONTAINER = "iso-profile-pic-container"; const C_BTN_PROFILE_PIC = "iso-profile-picture-btn"; const C_BTN_PROFILE_PIC_SPAN = "iso-profile-picture-span"; const S_IG_POST_CONTAINER_WITHOUT_BUTTON = `.${IG_C_POST_CONTAINER}:not(.${C_POST_WITH_BUTTON})`; const getIgUserInfoApiUrl = (userID) => `https://i.instagram.com/api/v1/users/${userID}/info/`; /* injects the needed CSS into DOM */ injectStyles(); /* triggered whenever a new instagram post is loaded on the feed */ document.arrive(S_IG_POST_CONTAINER_WITHOUT_BUTTON, (node) => { generatePostButton(node); }); /* triggered whenever a single post is opened (on a profile) */ document.arrive(`.${IG_C_SINGLE_POST_CONTAINER}`, (node) => { generatePostButton(node); }); /* triggered whenever a story is opened */ document.arrive(`.${IG_C_STORY_CONTAINER}`, (node) => { generateStoryButton(node); }); /* triggered whenever a profile page is loaded */ document.arrive(`.${IG_C_PROFILE_CONTAINER}`, (node) => { generateProfilePictureButton(node); }); /** * Window load callback * Checks if there are relevant nodes already loaded in DOM and performs the corresponding actions */ window.onload = ((e) => { if (/* is on post feed */ window.location.pathname === '/') { let postArticles = document.querySelectorAll(S_IG_POST_CONTAINER_WITHOUT_BUTTON); postArticles.forEach(node => generatePostButton(node)); } else if (/* is on single post page */ window.location.pathname.startsWith("/p/")) { let node = document.querySelector(`.${IG_C_SINGLE_POST_CONTAINER}`); if (node != null) { generatePostButton(node); } } else if (/* is on story page */ window.location.pathname.startsWith("/stories/")) { let node = document.querySelector(`.${IG_C_STORY_CONTAINER}`); if (node == null) { generateStoryButton(node); } } }) /** * Appends new elements to DOM containing the story source opening button * @param {Object} node DOM element node */ function generateStoryButton(node) { /* exits if the story button already exists */ if (elementExistsInNode(`.${C_BTN_STORY_CONTAINER}`, node)) return; try { let buttonStoryContainer = document.createElement("span"); let buttonStory = document.createElement("button"); buttonStoryContainer.classList.add(C_BTN_STORY_CONTAINER); buttonStory.classList.add(C_BTN_STORY); buttonStoryContainer.setAttribute("title", "Open source"); buttonStory.addEventListener("click", () => openStoryContent(node)); buttonStoryContainer.appendChild(buttonStory); node.appendChild(buttonStoryContainer); } catch (exception) { logError("Failed to generate story button", exception); } } /** * Appends new elements to DOM containing the post source opening button * @param {Object} node DOM element node */ function generatePostButton(node) { /* exits if the post button already exists */ if (elementExistsInNode(`.${C_BTN_POST_OUTER_SPAN}`, node)) return; try { let buttonsContainer = node.querySelector(IG_S_POST_BUTTONS); let newElementOuterSpan = document.createElement("span"); let newElementButton = document.createElement("button"); let newElementInnerSpan = document.createElement("span"); newElementOuterSpan.classList.add(C_BTN_POST_OUTER_SPAN); newElementButton.classList.add(C_BTN_POST); newElementInnerSpan.classList.add(C_BTN_POST_INNER_SPAN); newElementOuterSpan.setAttribute("title", "Open source"); newElementButton.addEventListener("click", () => openPostSourceFromSrcAttribute(node)); newElementButton.appendChild(newElementInnerSpan); newElementOuterSpan.appendChild(newElementButton); buttonsContainer.appendChild(newElementOuterSpan); node.classList.add(C_POST_WITH_BUTTON); } catch (exception) { logError("Failed to generate post button", exception); } } /** * Appends new elements to DOM containing the profile picture source opening button * @param {Object} node DOM element node */ function generateProfilePictureButton(node) { /* exits if the profile picture button already exists */ if (elementExistsInNode(`.${C_BTN_PROFILE_PIC_CONTAINER}`, node)) return; try { let profilePictureContainer = node.querySelector(`.${IG_C_PROFILE_PIC_CONTAINER}`); /* if the profile is private and the user isn't following or isn't logged in */ if (!profilePictureContainer) { profilePictureContainer = node.querySelector(`.${IG_C_PRIVATE_PROFILE_PIC_CONTAINER}`); } let newElementOuterSpan = document.createElement("span"); let newElementButton = document.createElement("button"); let newElementInnerSpan = document.createElement("span"); newElementOuterSpan.setAttribute("title", "Open full size picture"); newElementButton.addEventListener("click", e => { e.stopPropagation(); openProfilePictureSource(); }); newElementOuterSpan.classList.add(C_BTN_PROFILE_PIC_CONTAINER); newElementButton.classList.add(C_BTN_PROFILE_PIC); newElementInnerSpan.classList.add(C_BTN_PROFILE_PIC_SPAN); newElementButton.appendChild(newElementInnerSpan); newElementOuterSpan.appendChild(newElementButton); profilePictureContainer.appendChild(newElementOuterSpan); } catch (error) { logError(error); } } /** * Gets the story source url from the src attribute on the node and opens it in a new tab * @param {Object} node DOM element node */ function openStoryContent(node) { try { let container = node.querySelector(`.${IG_C_STORY_MEDIA_CONTAINER}`); let video = container.querySelector("video"); let image = container.querySelector("img"); if (video) { let videoElement = video.querySelector("source"); let videoSource = videoElement ? videoElement.getAttribute("src") : null; if (!videoSource) { throw "Video source isn't available"; } window.open(videoSource, "_blank"); } else if (image) { let imageSource = image.getAttribute("src"); if (!imageSource) { throw "Image source isn't available"; } window.open(imageSource, "_blank"); } else { throw "Story media isn't available" } } catch (exception) { showAndLogError("Failed to open story source", exception); } } /** * Gets the source url of a post from the src attribute on the node and opens it in a new tab * @param {Object} node DOM element node */ function openPostSourceFromSrcAttribute(node) { let nodeListItems = node.querySelectorAll(`.${IG_C_MULTI_POST_LIST_ITEM}`); try { if (/* is multi post */ nodeListItems.length != 0) { let scroller = node.querySelector(`.${IG_C_MULTI_POST_SCROLLER}`); let scrollerOffset = Math.abs((() => { let scrollerStyles = window.getComputedStyle(scroller); return parseInt(scrollerStyles.getPropertyValue("transform").split(",")[4]); })()); let mediaIndex = 0; if (scrollerOffset != 0) { let totalWidth = 0; nodeListItems.forEach(item => { let itemStyles = window.getComputedStyle(item); totalWidth += parseInt(itemStyles.getPropertyValue("width")); }); mediaIndex = ((scrollerOffset * nodeListItems.length) / totalWidth); } openPostMediaSource(nodeListItems[mediaIndex]); } else /* is single post */ { openPostMediaSource(node); } } catch (exception) { showAndLogError("Failed o open post source", exception); } } /** * Gets the source url of a post from the src attribute on the node and opens it in a new tab * @param {Object} node DOM element node */ function openPostMediaSource(node) { let image = node.querySelector(`.${IG_C_POST_IMG}`); let video = node.querySelector(`.${IG_C_POST_VIDEO}`); if (!image && !video) { throw "Failed to open source, no media found"; } window.open((video || image).getAttribute("src"), "_blank"); } /** * Gets the source url of a profile picture from the instagram API and opens it in a new tab */ function openProfilePictureSource() { let defaultErrorHandler = error => { document.body.style.cursor = "default"; alert("Couldn't get profile picture source"); logError(`Failed to get profile picture source: ${error}`); } try { let openImageFromUserInfo = response => { let hdImageURL = response.hd_profile_pic_url_info; if (hdImageURL != null) { window.open(hdImageURL.url, "_blank"); } document.body.style.cursor = "default"; }; let openImageFromUpdatedSharedData = () => { getUpdatedProfilePageData() .then(response => { getUserInfoFromAPI(response.id) .then(openImageFromUserInfo) .catch(defaultErrorHandler); }) .catch(defaultErrorHandler); }; let openImageFromUserInfoAPI = userData => { getUserInfoFromAPI(userData.id) .then(openImageFromUserInfo) .catch(error => { let sharedDataImageURL = userData.profile_pic_url_hd; if (sharedDataImageURL) { window.open(sharedDataImageURL, "_blank"); } else { defaultErrorHandler(error); } }); }; let openImageFromFreshHTMLPage = () => { getUserInfoFromFreshHTMLPage() .then(openImageFromUserInfoAPI) .catch(defaultErrorHandler); }; let pageUsername = document.querySelector(`.${IG_C_PROFILE_USERNAME_TITLE}`).innerText; let profilePageData = _sharedData.entry_data.ProfilePage; document.body.style.cursor = "wait"; /* if sharedData has any user information */ if (profilePageData) { let userSharedData = profilePageData[0].graphql.user; /* if sharedData is correct */ if (pageUsername === userSharedData.username) { /* getting user info from the api */ openImageFromUserInfoAPI(userSharedData); /* if the user is logged in */ } else if (_sharedData.config.viewer != null) { /* querying graphql directly to get user info*/ openImageFromUpdatedSharedData(); } else { /* getting a fresh new page and updated ProfilePage user data */ openImageFromFreshHTMLPage(); } } else { openImageFromFreshHTMLPage(); } } catch (error) { defaultErrorHandler(error); } } /** * Parses a whole HTML page as a last attempt to get the user id * - This function is only used when: * - The profile page is private * - The user isn't logged in * - The sharedData variable, which holds the profile user's id, isn't correct * @returns {Promise} Promise with the ProfilePage user data object or an error */ function getUserInfoFromFreshHTMLPage() { return new Promise((resolve, reject) => { httpGETRequest(window.location, false) .then(response => { try { let parser = new DOMParser(); let doc = parser.parseFromString(response, "text/html"); let allScripts = doc.querySelectorAll("script"); for (let i = 0; i < allScripts.length; i++) { if (/window._sharedData/.test(allScripts[i].innerText)) { let extractedJSON = /window._sharedData = (.+)/.exec(allScripts[i].innerText)[1]; extractedJSON = extractedJSON.slice(0, -1); let sharedData = JSON.parse(extractedJSON); let userInfo = sharedData.entry_data.ProfilePage[0].graphql.user; resolve(userInfo); break; } } } catch (error) { reject(error); } }) .catch(error => reject(error)); }); } /** * Requests user information from the instagram API * @param {number} userId * @returns {Promise} Promise with the user info object or an error */ function getUserInfoFromAPI(userId) { return new Promise((resolve, reject) => { httpGETRequest(getIgUserInfoApiUrl(userId)) .then(response => { let userInfo = response.user; resolve(userInfo); }) .catch(error => reject(error)) }) } /** * Requests the user profile page data from graphql * @returns {Promise} Promise with the graphql user info object or an error */ function getUpdatedProfilePageData() { return new Promise((resolve, reject) => { httpGETRequest(`${window.location}?__a=1`) .then(response => { let userSharedData = response.graphql.user; resolve(userSharedData); }) .catch(error => reject(error)) }); } /** * Performs an HTTP GET request using the GM_xmlhttpRequest function * @param {string} url * @param {boolean} [parseToJSON = true] default true * @returns {Promise} Promise object with the response text or an error */ function httpGETRequest(url, parseToJSON = true) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: res => { if (res.status === 200) { let response = res.responseText; if (parseToJSON) { response = JSON.parse(res.responseText); } resolve(response); } else { reject(`Status Code ${res.status} ${res.statusText.length > 0 ? ', ' + res.statusText : ''}`); } }, onerror: error => reject(error), ontimeout: () => reject("Request Timeout"), onabort: () => reject("Aborted") }) }) } /** * Matches a CSS selector against a DOM element object to check if the element exist in the node * @param {string} selector * @param {Object} node DOM element node * @returns {boolean} true if the element exists in the node, otherwise false */ function elementExistsInNode(selector, node) { return (node.querySelector(selector) != null); } /** * Appends the necessary style elements to DOM using the GM_addStyle function */ function injectStyles() { let b64icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfiAxwDOBTFNFQBAAABKklEQVQ4y6WTvUoDQRSFvwkbxCqijY2okEIEixQpBMHKykIFC8H/yiew8Rl8i4ClCoJYWGkhaGXjrkmTbsUiTQoVf45Ndp1lZ8eIp7vnnnPvnBkG/gjjb2uAOcoW0fplnvaVRccAaIFZhnPqW3OkMa4Zz84o60RunAFoQm2bDDhgmSsOHad7BjBtrXFjb3jUi0Y8KUYV2hvQly77kH/qKTFIF33Id5MsHoMl30njdwoNlnw75SqaLDC45EnLYbDkW/lZOYMl3wRQTTW/4bQn3+jVoUK/YUqxPrSe1pGin26QD2wizVM15+7LDlykadIseswSbwzhgUpUeLWJO0nTHsOSpIa1XSsc06VR8PnqrGKom3t7xp66KkasxUw+AA0y4/iiADEP5p3/4BuEXi9gkPrfQgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOC0wMy0yOFQwMzo1NjoyMCswMjowMO7sj9MAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTgtMDMtMjhUMDM6NTY6MjArMDI6MDCfsTdvAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg=="; let styles = [ `.${C_BTN_POST_OUTER_SPAN}{margin-left:10px;margin-right:-10px;}`, `.${C_BTN_POST}{outline:none;-webkit-box-align:center;align-items:center;background:0;border:0;cursor:pointer;display:flex;-webkit-box-flex:0;flex-grow:0;-webkit-box-pack:center;justify-content:center;min-height:40px;min-width:40px;padding:0;}`, `.${C_BTN_PROFILE_PIC}{outline:none;background-color:white;border:0;cursor:pointer;display:flex;-webkit-box-flex:0;flex-grow:0;-webkit-box-pack:center;justify-content:center;min-height:40px;min-width:40px;padding:0;border-radius:50%;transition:background-color .5s ease;-webkit-transition:background-color .5s ease;}`, `.${C_BTN_PROFILE_PIC}:hover{background-color:#D0D0D0;transition:background-color .5s ease;-webkit-transition:background-color .5s ease;}`, `.${C_BTN_POST_INNER_SPAN},.${C_BTN_PROFILE_PIC_SPAN}{display:block;background-repeat:no-repeat;background-position:100%-26px;height:24px;width:24px;background-image:url(/static/bundles/base/sprite_glyphs.png/4b550af4600d.png);cursor:pointer;}`, `.${C_BTN_STORY}{border:none;position:fixed;top:0;right:0;margin:20px;cursor:pointer;width:24px;height:24px;background-color:transparent;background-image:url(${b64icon})}`, `.${C_BTN_PROFILE_PIC_CONTAINER}{transition:.5s ease;opacity:0;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);text-align:center}`, `.${IG_C_PRIVATE_PIC_IMG_CONTAINER}>img{transition:.5s ease;backface-visibility:hidden;}`, `.${IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER}>img{transition:.5s ease;backface-visibility:hidden;}`, `.${IG_C_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC_CONTAINER}{opacity:1}`, `.${IG_C_PRIVATE_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC_CONTAINER}{opacity:1}` ]; styles.forEach((style) => GM_addStyle(style)); } /** * Shows an alert with an error message and logs an exception to the console * @param {string} error * @param {(Object|string)} exception */ function showAndLogError(error, exception) { showMessage(error); logError(error, exception); } /** * Shows an alert with a message * @param {string} message */ function showMessage(message) { alert(`Instagram Source Opener:\n\n${message}`); } /** * Logs an error string and exception to the console * @param {string} error * @param {(Object|string)} exception */ function logError(error, exception) { if (LOGGING_ENABLED) { console.error(`Instagram Source Opener:${ error ? '\n' + error : '' }${ exception ? '\n' + exception : '' }`); } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址