InstaDecrapper

Replaces Instagram pages with their decrapped versions (only media & titles)

  1. // ==UserScript==
  2. // @name InstaDecrapper
  3. // @version 1.3.2
  4. // @description Replaces Instagram pages with their decrapped versions (only media & titles)
  5. // @author GreasyPangolin
  6. // @license MIT
  7. // @match https://www.instagram.com/*
  8. // @match https://instagram.com/*
  9. // @match http://localhost:8000/*
  10. // @run-at document-start
  11. // @grant none
  12. // @namespace https://greasyfork.org/users/1448662
  13. // ==/UserScript==
  14.  
  15. function isDebug() {
  16. return window.location.href.includes('localhost')
  17. }
  18.  
  19. async function fixture(variables) {
  20. let url
  21. if (variables?.shortcode) {
  22. url = `http://localhost:8000/post_${variables.shortcode}.json`
  23. } else if (variables?.id) {
  24. url = `http://localhost:8000/next_page.json`
  25. } else {
  26. url = `http://localhost:8000/profile.json`
  27. }
  28.  
  29. const resp = await fetch(url)
  30. if (!resp.ok) {
  31. throw new Error(`Fixture fetch failed with status ${resp.status}: ${await resp.text()}`)
  32. }
  33.  
  34. return await resp.json()
  35. }
  36.  
  37. function filterNonNull(obj) {
  38. return Object.fromEntries(
  39. Object.entries(obj).filter(([_, v]) => v != null)
  40. );
  41. }
  42.  
  43. function extractDataAndRemoveScripts(runId) {
  44. // Extract CSRF token and App ID from scripts
  45. let csrfToken = ''
  46. let appId = ''
  47. let profileId = null
  48.  
  49. var scripts = document.querySelectorAll('script')
  50.  
  51. for (var i = 0; i < scripts.length; i++) {
  52. // scan for the script that contains the CSRF token and App ID
  53. const csrfMatch = scripts[i].textContent.match(/"csrf_token":"([^"]+)"/)
  54. const appIdMatch = scripts[i].textContent.match(/"app_id":"([^"]+)"/)
  55. const profileIdMatch = scripts[i].textContent.match(/"profile_id":"([^"]+)"/)
  56.  
  57. if (csrfMatch && csrfMatch[1]) {
  58. csrfToken = csrfMatch[1]
  59. console.log(`[Run ${runId}] Found CSRF token: ${csrfToken} `)
  60. }
  61.  
  62. if (appIdMatch && appIdMatch[1]) {
  63. appId = appIdMatch[1]
  64. console.log(`[Run ${runId}] Found App ID: ${appId} `)
  65. }
  66.  
  67. if (profileIdMatch && profileIdMatch[1]) {
  68. profileId = profileIdMatch[1]
  69. console.log(`[Run ${runId}] Found profile ID: ${profileId} `)
  70. }
  71.  
  72. // we don't need this script anymore
  73. scripts[i].remove()
  74.  
  75. if (csrfToken && appId && profileId) {
  76. return {
  77. secrets: { csrfToken, appId },
  78. profileId,
  79. }
  80. }
  81. }
  82.  
  83. // secrets found but profile ID is missing (possibly a post page)
  84. if (csrfToken && appId) {
  85. return {
  86. secrets: { csrfToken, appId },
  87. profileId: null,
  88. }
  89. }
  90.  
  91. console.log(`[Run ${runId}] Could not find CSRF token and App ID`)
  92.  
  93. return {
  94. secrets: null,
  95. profileId: null,
  96. }
  97. }
  98.  
  99. function renderProfileHeader(user) {
  100. const header = document.createElement('div')
  101. header.style.cssText = 'display: flex; align-items: center; padding: 20px;'
  102.  
  103. const info = document.createElement('div')
  104. info.style.display = 'flex'
  105. info.style.alignItems = 'start'
  106.  
  107. const profilePic = document.createElement('img')
  108. profilePic.src = user.profilePicUrl
  109. profilePic.width = 64
  110. profilePic.height = 64
  111. profilePic.style.borderRadius = '50%'
  112. profilePic.style.marginRight = '20px'
  113.  
  114. info.appendChild(profilePic)
  115.  
  116. const textInfo = document.createElement('div')
  117.  
  118. const nameContainer = document.createElement('div')
  119. nameContainer.style.display = 'flex'
  120. nameContainer.style.alignItems = 'center'
  121. nameContainer.style.gap = '5px'
  122.  
  123. const name = document.createElement('h1')
  124. name.textContent = user.fullName
  125. name.style.margin = '0 0 10px 0'
  126. name.style.fontFamily = 'sans-serif'
  127. name.style.fontSize = '18px'
  128.  
  129. nameContainer.appendChild(name)
  130.  
  131. if (user.isVerified) {
  132. const checkmark = document.createElement('span')
  133. checkmark.textContent = '✓'
  134. checkmark.style.margin = '0 0 10px'
  135. checkmark.style.color = '#00acff'
  136. checkmark.style.fontSize = '18px'
  137. checkmark.style.fontWeight = 'bold'
  138. nameContainer.appendChild(checkmark)
  139. }
  140.  
  141. textInfo.appendChild(nameContainer)
  142.  
  143. if (user.username) {
  144. const username = document.createElement('a')
  145.  
  146. username.href = '/' + user.username
  147. username.textContent = '@' + user.username
  148. username.style.margin = '0 0 10px 0'
  149. username.style.fontFamily = 'sans-serif'
  150. username.style.fontSize = '14px'
  151. username.style.textDecoration = 'none'
  152. username.style.color = '#00376b'
  153. username.target = '_blank'
  154.  
  155. textInfo.appendChild(username)
  156. }
  157.  
  158. if (user.biography) {
  159. const bio = document.createElement('p')
  160.  
  161. bio.textContent = user.biography
  162. bio.style.margin = '0 0 10px 0'
  163. bio.style.whiteSpace = 'pre-line'
  164. bio.style.fontFamily = 'sans-serif'
  165. bio.style.fontSize = '14px'
  166.  
  167. textInfo.appendChild(bio)
  168. }
  169.  
  170. if (user.bioLinks && user.bioLinks.length > 0) {
  171. const links = document.createElement('div')
  172.  
  173. user.bioLinks.forEach(link => {
  174. const a = document.createElement('a')
  175. a.href = link.url
  176. a.textContent = link.title
  177. a.target = '_blank'
  178. a.style.display = 'block'
  179. a.style.fontFamily = 'sans-serif'
  180. a.style.fontSize = '14px'
  181. links.appendChild(a)
  182. })
  183.  
  184. textInfo.appendChild(links)
  185. }
  186.  
  187. info.appendChild(textInfo)
  188.  
  189. header.appendChild(info)
  190.  
  191. document.body.appendChild(header)
  192. }
  193.  
  194. function parseMediaNode(media) {
  195. if (!media) return []; // Handle cases where media node might be null or undefined
  196.  
  197. const date = new Date(media.taken_at_timestamp * 1000).toISOString().slice(0, 19).replace('T', ' ')
  198. const title = media.edge_media_to_caption?.edges[0]?.node.text || "No title"
  199. const shortcode = media.shortcode
  200.  
  201. // Handle sidecar (carousel) posts
  202. if ((media.__typename === 'GraphSidecar' || media.__typename === 'XDTGraphSidecar') && media.edge_sidecar_to_children?.edges) {
  203. return media.edge_sidecar_to_children.edges.map(childEdge => {
  204. const child = childEdge.node
  205. if (!child) return null; // Skip if child node is invalid
  206. const isChildVideo = child.__typename === 'GraphVideo' || child.__typename === 'XDTGraphVideo'
  207. return {
  208. date: date, // Use parent's date
  209. title: title, // Use parent's title
  210. isVideo: isChildVideo,
  211. videoUrl: isChildVideo ? child.video_url : null,
  212. imageUrl: child.display_url,
  213. shortcode: shortcode // Use parent's shortcode
  214. }
  215. }).filter(item => item !== null); // Filter out any null items from invalid child nodes
  216. }
  217. // Handle single image or video posts
  218. else {
  219. const isVideo = media.is_video || media.__typename === 'GraphVideo' || media.__typename === 'XDTGraphVideo'
  220. return [{
  221. date: date,
  222. title: title,
  223. isVideo: isVideo,
  224. videoUrl: isVideo ? media.video_url : null,
  225. imageUrl: media.display_url,
  226. shortcode: shortcode
  227. }]
  228. }
  229. }
  230.  
  231. function renderMedia(mediaItems) {
  232. const mediaContainer = document.createElement('div')
  233. mediaContainer.style.display = 'grid'
  234. mediaContainer.style.gridTemplateColumns = 'repeat(auto-fill, minmax(320px, 1fr))'
  235. mediaContainer.style.gap = '20px'
  236. mediaContainer.style.padding = '20px'
  237.  
  238. mediaItems.forEach(item => {
  239. const mediaDiv = document.createElement('div')
  240. mediaDiv.className = 'media'
  241. mediaDiv.style.display = 'flex'
  242. mediaDiv.style.flexDirection = 'column'
  243. mediaDiv.style.alignItems = 'center'
  244.  
  245. if (item.isVideo) {
  246. const videoElement = document.createElement('video')
  247. videoElement.controls = true
  248. videoElement.width = 320
  249.  
  250. const source = document.createElement('source')
  251. source.src = item.videoUrl
  252. source.type = 'video/mp4'
  253.  
  254. videoElement.appendChild(source)
  255. mediaDiv.appendChild(videoElement)
  256. } else {
  257. const imageElement = document.createElement('img')
  258. imageElement.src = item.imageUrl
  259. imageElement.width = 320
  260. imageElement.style.height = 'auto'
  261.  
  262. const linkElement = document.createElement('a')
  263. linkElement.href = item.imageUrl
  264. linkElement.target = '_blank'
  265.  
  266. linkElement.appendChild(imageElement)
  267. mediaDiv.appendChild(linkElement)
  268. }
  269.  
  270. const dateContainer = document.createElement('div')
  271. dateContainer.style.display = 'flex'
  272. dateContainer.style.alignItems = 'center'
  273. dateContainer.style.justifyContent = 'center'
  274. dateContainer.style.gap = '10px'
  275. dateContainer.style.width = '320px'
  276.  
  277. const date = document.createElement('p')
  278. date.textContent = item.date
  279. date.style.fontFamily = 'sans-serif'
  280. date.style.fontSize = '12px'
  281. date.style.margin = '5px 0'
  282.  
  283. dateContainer.appendChild(date)
  284.  
  285. if (item.shortcode) {
  286. const postLink = document.createElement('a')
  287. postLink.href = `/p/${item.shortcode}`
  288. postLink.textContent = '[post]'
  289. postLink.style.fontFamily = 'sans-serif'
  290. postLink.style.fontSize = '12px'
  291. postLink.style.color = 'blue'
  292. postLink.style.textDecoration = 'none'
  293. dateContainer.appendChild(postLink)
  294. }
  295.  
  296. if (item.isVideo) {
  297. const previewLink = document.createElement('a')
  298. previewLink.href = item.imageUrl
  299. previewLink.textContent = '[preview]'
  300. previewLink.style.fontFamily = 'sans-serif'
  301. previewLink.style.fontSize = '12px'
  302. previewLink.style.color = 'blue'
  303. previewLink.style.textDecoration = 'none'
  304. dateContainer.appendChild(previewLink)
  305. }
  306.  
  307. mediaDiv.appendChild(dateContainer)
  308.  
  309. const title = document.createElement('p')
  310. title.textContent = item.title
  311. title.style.fontFamily = 'sans-serif'
  312. title.style.fontSize = '12px'
  313. title.style.width = '320px'
  314. title.style.textAlign = 'center'
  315.  
  316. mediaDiv.appendChild(title)
  317. mediaContainer.appendChild(mediaDiv)
  318. })
  319.  
  320. document.body.appendChild(mediaContainer)
  321. }
  322.  
  323. function renderLine() {
  324. const line = document.createElement('hr')
  325. line.style.margin = '20px 0'
  326. document.body.appendChild(line)
  327. }
  328.  
  329.  
  330. function renderLoadMoreButton(secrets, profileId, pageInfo) {
  331. let loadMoreButton = document.getElementById('loadMoreBtn')
  332.  
  333. // Remove old button
  334. if (loadMoreButton) {
  335. loadMoreButton.remove()
  336. }
  337.  
  338. // Add new "Load More" button
  339. if (pageInfo?.has_next_page && pageInfo.end_cursor) {
  340. // Create a horizontal line
  341. renderLine()
  342.  
  343. // Create "Load More" button
  344. loadMoreButton = document.createElement('button')
  345. loadMoreButton.id = 'loadMoreBtn'
  346. loadMoreButton.style.cssText = 'display: block; margin: 20px auto; padding: 10px 20px; font-size: 16px; cursor: pointer;'
  347. document.body.appendChild(loadMoreButton)
  348.  
  349. // Update button's state and event listener
  350. loadMoreButton.textContent = 'Load More'
  351. loadMoreButton.disabled = false
  352.  
  353. // Clone and replace to ensure the event listener is fresh and doesn't stack
  354. const newButton = loadMoreButton.cloneNode(true)
  355. loadMoreButton.parentNode.replaceChild(newButton, loadMoreButton)
  356.  
  357. newButton.onclick = () => {
  358. newButton.disabled = true; // Prevent multiple clicks while loading
  359. newButton.textContent = 'Loading...'
  360. // Call loadNextPage with the new cursor
  361. loadNextPage(secrets, profileId, pageInfo.end_cursor)
  362. }
  363. }
  364. }
  365.  
  366. // Helper function for GraphQL API calls
  367. async function fetchGraphQL({ csrfToken, appId }, { variables, doc_id }) {
  368. if (isDebug()) {
  369. return fixture(variables)
  370. }
  371.  
  372. const resp = await fetch(`https://www.instagram.com/graphql/query`, {
  373. "method": "POST",
  374. "credentials": "include",
  375. "headers": {
  376. "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0",
  377. "Accept": "*/*",
  378. "Accept-Language": "en-US,en;q=0.5",
  379. "Content-Type": "application/x-www-form-urlencoded",
  380. "X-CSRFToken": csrfToken,
  381. "X-IG-App-ID": appId,
  382. "Origin": "https://www.instagram.com",
  383. "Sec-Fetch-Dest": "empty",
  384. "Sec-Fetch-Mode": "cors",
  385. "Sec-Fetch-Site": "same-origin",
  386. },
  387. "body": new URLSearchParams({
  388. "av": "0",
  389. "hl": "en",
  390. "__d": "www",
  391. "__user": "0",
  392. "__a": "1",
  393. "__req": "a",
  394. "__hs": "20168.HYP:instagram_web_pkg.2.1...0",
  395. "dpr": "2",
  396. "__ccg": "EXCELLENT",
  397. "fb_api_caller_class": "RelayModern",
  398. "variables": JSON.stringify(filterNonNull(variables)),
  399. "server_timestamps": "true",
  400. "doc_id": doc_id,
  401. }).toString()
  402. })
  403.  
  404. if (!resp.ok) {
  405. throw new Error(`GraphQL fetch failed with status ${resp.status}: ${await resp.text()}`)
  406. }
  407.  
  408. return await resp.json()
  409. }
  410.  
  411. // Helper function for standard Web API calls
  412. async function fetchProfile({ csrfToken, appId }, username) {
  413. if (isDebug()) {
  414. return fixture()
  415. }
  416.  
  417. const resp = await fetch(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${username}&hl=en`, {
  418. method: 'GET',
  419. credentials: "include",
  420. headers: {
  421. "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0",
  422. "Accept": "*/*",
  423. "Accept-Language": "en,en-US;q=0.5",
  424. "X-CSRFToken": csrfToken,
  425. "X-IG-App-ID": appId,
  426. "X-IG-WWW-Claim": "0",
  427. "X-Requested-With": "XMLHttpRequest",
  428. "Alt-Used": "www.instagram.com",
  429. "Sec-Fetch-Dest": "empty",
  430. "Sec-Fetch-Mode": "cors",
  431. "Sec-Fetch-Site": "same-origin",
  432. "Pragma": "no-cache",
  433. "Cache-Control": "no-cache"
  434. },
  435. referrer: `https://www.instagram.com/${username}/?hl=en`,
  436. mode: "cors",
  437. })
  438.  
  439. if (!resp.ok) {
  440. throw new Error(`API fetch failed with status ${resp.status}: ${await resp.text()}`)
  441. }
  442.  
  443. return await resp.json()
  444. }
  445.  
  446.  
  447. async function loadSinglePost(secrets, shortcode) {
  448. const data = await fetchGraphQL(secrets, {
  449. doc_id: "8845758582119845",
  450. variables: {
  451. "shortcode": shortcode,
  452. "fetch_tagged_user_count": null,
  453. "hoisted_comment_id": null,
  454. "hoisted_reply_id": null
  455. },
  456. })
  457.  
  458. // Check if media data exists
  459. if (!data || !data.data || !data.data.xdt_shortcode_media) {
  460. console.error(`Media data is missing or invalid for post ${shortcode}:`, data)
  461. 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>`
  462. return; // Stop execution
  463. }
  464.  
  465. const media = data.data.xdt_shortcode_media
  466.  
  467. // Use the new parsing function
  468. const mediaItems = parseMediaNode(media)
  469.  
  470. renderProfileHeader({
  471. username: media.owner.username,
  472. fullName: media.owner.full_name,
  473. profilePicUrl: media.owner.profile_pic_url,
  474. isVerified: media.owner.is_verified
  475. })
  476.  
  477. renderMedia(mediaItems)
  478. }
  479.  
  480.  
  481. // Refactored loadNextPage
  482. async function loadNextPage(secrets, profileId, after) {
  483. const data = await fetchGraphQL(secrets, {
  484. doc_id: "7950326061742207",
  485. variables: {
  486. "id": profileId,
  487. "after": after,
  488. "first": 12 // Number of posts to fetch per page
  489. },
  490. })
  491.  
  492. // Parse `data` and fill `mediaItems` using the parsing function
  493. const mediaEdges = data?.data?.user?.edge_owner_to_timeline_media?.edges
  494. const mediaItems = mediaEdges ? mediaEdges.flatMap(edge => parseMediaNode(edge.node)) : []
  495.  
  496. if (!mediaEdges) {
  497. console.error("Could not find media edges in the response data:", data)
  498. }
  499.  
  500. renderMedia(mediaItems)
  501.  
  502. // Handle pagination
  503. const pageInfo = data?.data?.user?.edge_owner_to_timeline_media?.page_info
  504. renderLoadMoreButton(secrets, profileId, pageInfo)
  505. }
  506.  
  507. async function loadFullProfile(secrets, username) {
  508. const data = await fetchProfile(secrets, username)
  509.  
  510. // Check if user data exists
  511. if (!data || !data.data || !data.data.user) {
  512. console.error("Profile data is missing or invalid:", data)
  513. 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>`
  514. return; // Stop execution
  515. }
  516.  
  517. // Header
  518. const user = data.data.user
  519.  
  520. renderProfileHeader({
  521. fullName: user.full_name,
  522. biography: user.biography,
  523. profilePicUrl: user.profile_pic_url_hd || user.profile_pic_url, // Fallback for profile pic
  524. bioLinks: user.bio_links,
  525. isVerified: user.is_verified,
  526. })
  527.  
  528. // Stories or whatever
  529. const felixVideoEdges = user.edge_felix_video_timeline?.edges || []
  530. if (felixVideoEdges.length > 0) {
  531. renderMedia(felixVideoEdges.flatMap(edge => parseMediaNode(edge.node)))
  532. renderLine()
  533. }
  534.  
  535. // Timeline
  536. const timelineEdges = user.edge_owner_to_timeline_media?.edges || []
  537. const timelineMedia = timelineEdges.flatMap(edge => parseMediaNode(edge?.node)); // Add null check for edge
  538.  
  539. renderMedia(timelineMedia)
  540.  
  541. // Show more button
  542. const pageInfo = user.edge_owner_to_timeline_media?.page_info
  543. const profileId = user.id
  544.  
  545. renderLoadMoreButton(secrets, profileId, pageInfo)
  546. }
  547.  
  548. function run({ secrets, profileId }) {
  549. // first, stop the page from loading
  550. window.stop()
  551.  
  552. document.head.innerHTML = ''
  553. document.body.innerHTML = ''
  554.  
  555. // and now execute our code
  556. const postID = window.location.pathname.match(/(?:p|reel)\/([^\/]*)/)
  557.  
  558. if (postID) {
  559. const shortcode = postID[1]
  560. console.log(`Loading post: ${shortcode}`)
  561. loadSinglePost(secrets, shortcode)
  562. } else {
  563. const username = window.location.pathname.split('/')[1]
  564. console.log(`Loading profile: ${username}`)
  565. try {
  566. loadFullProfile(secrets, username)
  567. } catch (error) {
  568. console.error("Error loading full profile:", error)
  569.  
  570. // most probably access errro, let's try loading a limited profile
  571. loadNextPage(secrets, profileId, null)
  572. }
  573. }
  574. }
  575.  
  576. (function () {
  577. 'use strict'
  578.  
  579. if (isDebug()) {
  580. console.log("Debug mode enabled")
  581. document.body.innerHTML = ""
  582.  
  583. const shortcode = window.location.pathname.split('/').pop()
  584. if (shortcode && shortcode == "limited") {
  585. loadNextPage({ /* no secrets */ }, profileId)
  586. } else if (shortcode) {
  587. loadSinglePost({ /* no secrets */ }, shortcode)
  588. } else {
  589. loadFullProfile({/* no secrets */ })
  590. }
  591.  
  592. return
  593. }
  594.  
  595. // let's try to stop it from blinking
  596. const style = document.createElement('style')
  597. style.textContent = '#splash-screen { display: none !important; }'
  598. document.head.appendChild(style)
  599.  
  600. // we try to extract the secrets and run the app right away,
  601. // sometimes it works :)
  602. const { secrets, profileId } = extractDataAndRemoveScripts(1)
  603. if (!secrets) {
  604. // but since the user-script injection is kinda unpredictable
  605. // especially across different browsers and extensions,
  606. // we also fallback to a DOMContentLoaded event listener
  607. document.addEventListener('DOMContentLoaded', function () {
  608. window.stop() // we know that the secrets are in the DOM, so we can stop loading all other garbage
  609.  
  610. const { secrets, profileId } = extractDataAndRemoveScripts(2)
  611. if (!secrets) {
  612. console.log("Failed to extract secrets")
  613. return
  614. }
  615.  
  616. run({ secrets, profileId })
  617. })
  618.  
  619. return
  620. }
  621.  
  622. run(secrets)
  623. })()