您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Replaces Instagram pages with their decrapped versions (only media & titles)
// ==UserScript== // @name InstaDecrapper // @version 1.3.2 // @description Replaces Instagram pages with their decrapped versions (only media & titles) // @author GreasyPangolin // @license MIT // @match https://www.instagram.com/* // @match https://instagram.com/* // @match http://localhost:8000/* // @run-at document-start // @grant none // @namespace https://greasyfork.org/users/1448662 // ==/UserScript== function isDebug() { return window.location.href.includes('localhost') } async function fixture(variables) { let url if (variables?.shortcode) { url = `http://localhost:8000/post_${variables.shortcode}.json` } else if (variables?.id) { url = `http://localhost:8000/next_page.json` } else { url = `http://localhost:8000/profile.json` } const resp = await fetch(url) if (!resp.ok) { throw new Error(`Fixture fetch failed with status ${resp.status}: ${await resp.text()}`) } return await resp.json() } function filterNonNull(obj) { return Object.fromEntries( Object.entries(obj).filter(([_, v]) => v != null) ); } function extractDataAndRemoveScripts(runId) { // Extract CSRF token and App ID from scripts let csrfToken = '' let appId = '' let profileId = null var scripts = document.querySelectorAll('script') for (var i = 0; i < scripts.length; i++) { // scan for the script that contains the CSRF token and App ID const csrfMatch = scripts[i].textContent.match(/"csrf_token":"([^"]+)"/) const appIdMatch = scripts[i].textContent.match(/"app_id":"([^"]+)"/) const profileIdMatch = scripts[i].textContent.match(/"profile_id":"([^"]+)"/) if (csrfMatch && csrfMatch[1]) { csrfToken = csrfMatch[1] console.log(`[Run ${runId}] Found CSRF token: ${csrfToken} `) } if (appIdMatch && appIdMatch[1]) { appId = appIdMatch[1] console.log(`[Run ${runId}] Found App ID: ${appId} `) } if (profileIdMatch && profileIdMatch[1]) { profileId = profileIdMatch[1] console.log(`[Run ${runId}] Found profile ID: ${profileId} `) } // we don't need this script anymore scripts[i].remove() if (csrfToken && appId && profileId) { return { secrets: { csrfToken, appId }, profileId, } } } // secrets found but profile ID is missing (possibly a post page) if (csrfToken && appId) { return { secrets: { csrfToken, appId }, profileId: null, } } console.log(`[Run ${runId}] Could not find CSRF token and App ID`) return { secrets: null, profileId: null, } } function renderProfileHeader(user) { const header = document.createElement('div') header.style.cssText = 'display: flex; align-items: center; padding: 20px;' const info = document.createElement('div') info.style.display = 'flex' info.style.alignItems = 'start' const profilePic = document.createElement('img') profilePic.src = user.profilePicUrl profilePic.width = 64 profilePic.height = 64 profilePic.style.borderRadius = '50%' profilePic.style.marginRight = '20px' info.appendChild(profilePic) const textInfo = document.createElement('div') const nameContainer = document.createElement('div') nameContainer.style.display = 'flex' nameContainer.style.alignItems = 'center' nameContainer.style.gap = '5px' const name = document.createElement('h1') name.textContent = user.fullName name.style.margin = '0 0 10px 0' name.style.fontFamily = 'sans-serif' name.style.fontSize = '18px' nameContainer.appendChild(name) if (user.isVerified) { const checkmark = document.createElement('span') checkmark.textContent = '✓' checkmark.style.margin = '0 0 10px' checkmark.style.color = '#00acff' checkmark.style.fontSize = '18px' checkmark.style.fontWeight = 'bold' nameContainer.appendChild(checkmark) } textInfo.appendChild(nameContainer) if (user.username) { const username = document.createElement('a') username.href = '/' + user.username username.textContent = '@' + user.username username.style.margin = '0 0 10px 0' username.style.fontFamily = 'sans-serif' username.style.fontSize = '14px' username.style.textDecoration = 'none' username.style.color = '#00376b' username.target = '_blank' textInfo.appendChild(username) } if (user.biography) { const bio = document.createElement('p') bio.textContent = user.biography bio.style.margin = '0 0 10px 0' bio.style.whiteSpace = 'pre-line' bio.style.fontFamily = 'sans-serif' bio.style.fontSize = '14px' textInfo.appendChild(bio) } if (user.bioLinks && user.bioLinks.length > 0) { const links = document.createElement('div') user.bioLinks.forEach(link => { const a = document.createElement('a') a.href = link.url a.textContent = link.title a.target = '_blank' a.style.display = 'block' a.style.fontFamily = 'sans-serif' a.style.fontSize = '14px' links.appendChild(a) }) textInfo.appendChild(links) } info.appendChild(textInfo) header.appendChild(info) document.body.appendChild(header) } function parseMediaNode(media) { if (!media) return []; // Handle cases where media node might be null or undefined const date = new Date(media.taken_at_timestamp * 1000).toISOString().slice(0, 19).replace('T', ' ') const title = media.edge_media_to_caption?.edges[0]?.node.text || "No title" const shortcode = media.shortcode // Handle sidecar (carousel) posts if ((media.__typename === 'GraphSidecar' || media.__typename === 'XDTGraphSidecar') && media.edge_sidecar_to_children?.edges) { return media.edge_sidecar_to_children.edges.map(childEdge => { const child = childEdge.node if (!child) return null; // Skip if child node is invalid const isChildVideo = child.__typename === 'GraphVideo' || child.__typename === 'XDTGraphVideo' return { date: date, // Use parent's date title: title, // Use parent's title isVideo: isChildVideo, videoUrl: isChildVideo ? child.video_url : null, imageUrl: child.display_url, shortcode: shortcode // Use parent's shortcode } }).filter(item => item !== null); // Filter out any null items from invalid child nodes } // Handle single image or video posts else { const isVideo = media.is_video || media.__typename === 'GraphVideo' || media.__typename === 'XDTGraphVideo' return [{ date: date, title: title, isVideo: isVideo, videoUrl: isVideo ? media.video_url : null, imageUrl: media.display_url, shortcode: shortcode }] } } function renderMedia(mediaItems) { const mediaContainer = document.createElement('div') mediaContainer.style.display = 'grid' mediaContainer.style.gridTemplateColumns = 'repeat(auto-fill, minmax(320px, 1fr))' mediaContainer.style.gap = '20px' mediaContainer.style.padding = '20px' mediaItems.forEach(item => { const mediaDiv = document.createElement('div') mediaDiv.className = 'media' mediaDiv.style.display = 'flex' mediaDiv.style.flexDirection = 'column' mediaDiv.style.alignItems = 'center' if (item.isVideo) { const videoElement = document.createElement('video') videoElement.controls = true videoElement.width = 320 const source = document.createElement('source') source.src = item.videoUrl source.type = 'video/mp4' videoElement.appendChild(source) mediaDiv.appendChild(videoElement) } else { const imageElement = document.createElement('img') imageElement.src = item.imageUrl imageElement.width = 320 imageElement.style.height = 'auto' const linkElement = document.createElement('a') linkElement.href = item.imageUrl linkElement.target = '_blank' linkElement.appendChild(imageElement) mediaDiv.appendChild(linkElement) } const dateContainer = document.createElement('div') dateContainer.style.display = 'flex' dateContainer.style.alignItems = 'center' dateContainer.style.justifyContent = 'center' dateContainer.style.gap = '10px' dateContainer.style.width = '320px' const date = document.createElement('p') date.textContent = item.date date.style.fontFamily = 'sans-serif' date.style.fontSize = '12px' date.style.margin = '5px 0' dateContainer.appendChild(date) if (item.shortcode) { const postLink = document.createElement('a') postLink.href = `/p/${item.shortcode}` postLink.textContent = '[post]' postLink.style.fontFamily = 'sans-serif' postLink.style.fontSize = '12px' postLink.style.color = 'blue' postLink.style.textDecoration = 'none' dateContainer.appendChild(postLink) } if (item.isVideo) { const previewLink = document.createElement('a') previewLink.href = item.imageUrl previewLink.textContent = '[preview]' previewLink.style.fontFamily = 'sans-serif' previewLink.style.fontSize = '12px' previewLink.style.color = 'blue' previewLink.style.textDecoration = 'none' dateContainer.appendChild(previewLink) } mediaDiv.appendChild(dateContainer) const title = document.createElement('p') title.textContent = item.title title.style.fontFamily = 'sans-serif' title.style.fontSize = '12px' title.style.width = '320px' title.style.textAlign = 'center' mediaDiv.appendChild(title) mediaContainer.appendChild(mediaDiv) }) document.body.appendChild(mediaContainer) } function renderLine() { const line = document.createElement('hr') line.style.margin = '20px 0' document.body.appendChild(line) } function renderLoadMoreButton(secrets, profileId, pageInfo) { let loadMoreButton = document.getElementById('loadMoreBtn') // Remove old button if (loadMoreButton) { loadMoreButton.remove() } // Add new "Load More" button if (pageInfo?.has_next_page && pageInfo.end_cursor) { // Create a horizontal line renderLine() // Create "Load More" button loadMoreButton = document.createElement('button') loadMoreButton.id = 'loadMoreBtn' loadMoreButton.style.cssText = 'display: block; margin: 20px auto; padding: 10px 20px; font-size: 16px; cursor: pointer;' document.body.appendChild(loadMoreButton) // Update button's state and event listener loadMoreButton.textContent = 'Load More' loadMoreButton.disabled = false // Clone and replace to ensure the event listener is fresh and doesn't stack const newButton = loadMoreButton.cloneNode(true) loadMoreButton.parentNode.replaceChild(newButton, loadMoreButton) newButton.onclick = () => { newButton.disabled = true; // Prevent multiple clicks while loading newButton.textContent = 'Loading...' // Call loadNextPage with the new cursor loadNextPage(secrets, profileId, pageInfo.end_cursor) } } } // Helper function for GraphQL API calls async function fetchGraphQL({ csrfToken, appId }, { variables, doc_id }) { if (isDebug()) { return fixture(variables) } const resp = await fetch(`https://www.instagram.com/graphql/query`, { "method": "POST", "credentials": "include", "headers": { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Content-Type": "application/x-www-form-urlencoded", "X-CSRFToken": csrfToken, "X-IG-App-ID": appId, "Origin": "https://www.instagram.com", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", }, "body": new URLSearchParams({ "av": "0", "hl": "en", "__d": "www", "__user": "0", "__a": "1", "__req": "a", "__hs": "20168.HYP:instagram_web_pkg.2.1...0", "dpr": "2", "__ccg": "EXCELLENT", "fb_api_caller_class": "RelayModern", "variables": JSON.stringify(filterNonNull(variables)), "server_timestamps": "true", "doc_id": doc_id, }).toString() }) if (!resp.ok) { throw new Error(`GraphQL fetch failed with status ${resp.status}: ${await resp.text()}`) } return await resp.json() } // Helper function for standard Web API calls async function fetchProfile({ csrfToken, appId }, username) { if (isDebug()) { return fixture() } const resp = await fetch(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${username}&hl=en`, { method: 'GET', credentials: "include", headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0", "Accept": "*/*", "Accept-Language": "en,en-US;q=0.5", "X-CSRFToken": csrfToken, "X-IG-App-ID": appId, "X-IG-WWW-Claim": "0", "X-Requested-With": "XMLHttpRequest", "Alt-Used": "www.instagram.com", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Pragma": "no-cache", "Cache-Control": "no-cache" }, referrer: `https://www.instagram.com/${username}/?hl=en`, mode: "cors", }) if (!resp.ok) { throw new Error(`API fetch failed with status ${resp.status}: ${await resp.text()}`) } return await resp.json() } async function loadSinglePost(secrets, shortcode) { const data = await fetchGraphQL(secrets, { doc_id: "8845758582119845", variables: { "shortcode": shortcode, "fetch_tagged_user_count": null, "hoisted_comment_id": null, "hoisted_reply_id": null }, }) // Check if media data exists if (!data || !data.data || !data.data.xdt_shortcode_media) { console.error(`Media data is missing or invalid for post ${shortcode}:`, data) document.body.innerHTML = `<p style="color: orange; font-family: sans-serif; padding: 20px;">Could not find media data for post ${shortcode}. It might be private or unavailable.</p>` return; // Stop execution } const media = data.data.xdt_shortcode_media // Use the new parsing function const mediaItems = parseMediaNode(media) renderProfileHeader({ username: media.owner.username, fullName: media.owner.full_name, profilePicUrl: media.owner.profile_pic_url, isVerified: media.owner.is_verified }) renderMedia(mediaItems) } // Refactored loadNextPage async function loadNextPage(secrets, profileId, after) { const data = await fetchGraphQL(secrets, { doc_id: "7950326061742207", variables: { "id": profileId, "after": after, "first": 12 // Number of posts to fetch per page }, }) // Parse `data` and fill `mediaItems` using the parsing function const mediaEdges = data?.data?.user?.edge_owner_to_timeline_media?.edges const mediaItems = mediaEdges ? mediaEdges.flatMap(edge => parseMediaNode(edge.node)) : [] if (!mediaEdges) { console.error("Could not find media edges in the response data:", data) } renderMedia(mediaItems) // Handle pagination const pageInfo = data?.data?.user?.edge_owner_to_timeline_media?.page_info renderLoadMoreButton(secrets, profileId, pageInfo) } async function loadFullProfile(secrets, username) { const data = await fetchProfile(secrets, username) // Check if user data exists if (!data || !data.data || !data.data.user) { console.error("Profile data is missing or invalid:", data) document.body.innerHTML = `<p style="color: orange; font-family: sans-serif; padding: 20px;">Could not find profile data for ${username}. The profile might be private or does not exist.</p>` return; // Stop execution } // Header const user = data.data.user renderProfileHeader({ fullName: user.full_name, biography: user.biography, profilePicUrl: user.profile_pic_url_hd || user.profile_pic_url, // Fallback for profile pic bioLinks: user.bio_links, isVerified: user.is_verified, }) // Stories or whatever const felixVideoEdges = user.edge_felix_video_timeline?.edges || [] if (felixVideoEdges.length > 0) { renderMedia(felixVideoEdges.flatMap(edge => parseMediaNode(edge.node))) renderLine() } // Timeline const timelineEdges = user.edge_owner_to_timeline_media?.edges || [] const timelineMedia = timelineEdges.flatMap(edge => parseMediaNode(edge?.node)); // Add null check for edge renderMedia(timelineMedia) // Show more button const pageInfo = user.edge_owner_to_timeline_media?.page_info const profileId = user.id renderLoadMoreButton(secrets, profileId, pageInfo) } function run({ secrets, profileId }) { // first, stop the page from loading window.stop() document.head.innerHTML = '' document.body.innerHTML = '' // and now execute our code const postID = window.location.pathname.match(/(?:p|reel)\/([^\/]*)/) if (postID) { const shortcode = postID[1] console.log(`Loading post: ${shortcode}`) loadSinglePost(secrets, shortcode) } else { const username = window.location.pathname.split('/')[1] console.log(`Loading profile: ${username}`) try { loadFullProfile(secrets, username) } catch (error) { console.error("Error loading full profile:", error) // most probably access errro, let's try loading a limited profile loadNextPage(secrets, profileId, null) } } } (function () { 'use strict' if (isDebug()) { console.log("Debug mode enabled") document.body.innerHTML = "" const shortcode = window.location.pathname.split('/').pop() if (shortcode && shortcode == "limited") { loadNextPage({ /* no secrets */ }, profileId) } else if (shortcode) { loadSinglePost({ /* no secrets */ }, shortcode) } else { loadFullProfile({/* no secrets */ }) } return } // let's try to stop it from blinking const style = document.createElement('style') style.textContent = '#splash-screen { display: none !important; }' document.head.appendChild(style) // we try to extract the secrets and run the app right away, // sometimes it works :) const { secrets, profileId } = extractDataAndRemoveScripts(1) if (!secrets) { // but since the user-script injection is kinda unpredictable // especially across different browsers and extensions, // we also fallback to a DOMContentLoaded event listener document.addEventListener('DOMContentLoaded', function () { window.stop() // we know that the secrets are in the DOM, so we can stop loading all other garbage const { secrets, profileId } = extractDataAndRemoveScripts(2) if (!secrets) { console.log("Failed to extract secrets") return } run({ secrets, profileId }) }) return } run(secrets) })()