- // ==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)
- })()