InstaDecrapper

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

当前为 2025-03-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name InstaDecrapper
  3. // @version 1.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 extractSecretsAndRemoveScripts(runId) {
  16. // Extract CSRF token and App ID from scripts
  17. let csrfToken = ''
  18. let appId = ''
  19.  
  20. var scripts = document.querySelectorAll('script')
  21.  
  22. for (var i = 0; i < scripts.length; i++) {
  23. // scan for the script that contains the CSRF token and App ID
  24. const csrfMatch = scripts[i].textContent.match(/"csrf_token":"([^"]+)"/)
  25. const appIdMatch = scripts[i].textContent.match(/"app_id":"([^"]+)"/)
  26.  
  27. if (csrfMatch && csrfMatch[1]) {
  28. csrfToken = csrfMatch[1]
  29. console.log(`[Run ${runId}] Found CSRF token: ${csrfToken}`)
  30. }
  31.  
  32. if (appIdMatch && appIdMatch[1]) {
  33. appId = appIdMatch[1]
  34. console.log(`[Run ${runId}] Found App ID: ${appId}`)
  35. }
  36.  
  37. // we don't need this script anymore
  38. scripts[i].remove()
  39.  
  40. if (csrfToken && appId) {
  41. return { csrfToken, appId }
  42. }
  43. }
  44.  
  45. console.log(`[Run ${runId}] Could not find CSRF token and App ID`)
  46.  
  47. return null
  48. }
  49.  
  50. function renderProfileHeader(user) {
  51. const header = document.createElement('div')
  52. header.style.cssText = 'display: flex; align-items: center; padding: 20px;'
  53.  
  54. const info = document.createElement('div')
  55. info.style.display = 'flex'
  56. info.style.alignItems = 'start'
  57.  
  58. const profilePic = document.createElement('img')
  59. profilePic.src = user.profilePicUrl
  60. profilePic.width = 64
  61. profilePic.height = 64
  62. profilePic.style.borderRadius = '50%'
  63. profilePic.style.marginRight = '20px'
  64.  
  65. info.appendChild(profilePic)
  66.  
  67. const textInfo = document.createElement('div')
  68.  
  69. const nameContainer = document.createElement('div')
  70. nameContainer.style.display = 'flex'
  71. nameContainer.style.alignItems = 'center'
  72. nameContainer.style.gap = '5px'
  73.  
  74. const name = document.createElement('h1')
  75. name.textContent = user.fullName
  76. name.style.margin = '0 0 10px 0'
  77. name.style.fontFamily = 'sans-serif'
  78. name.style.fontSize = '18px'
  79.  
  80. nameContainer.appendChild(name)
  81.  
  82. if (user.isVerified) {
  83. const checkmark = document.createElement('span')
  84. checkmark.textContent = '✓'
  85. checkmark.style.margin = '0 0 10px'
  86. checkmark.style.color = '#00acff'
  87. checkmark.style.fontSize = '18px'
  88. checkmark.style.fontWeight = 'bold'
  89. nameContainer.appendChild(checkmark)
  90. }
  91.  
  92. textInfo.appendChild(nameContainer)
  93.  
  94. if (user.username) {
  95. const username = document.createElement('a')
  96.  
  97. username.href = '/' + user.username
  98. username.textContent = '@' + user.username
  99. username.style.margin = '0 0 10px 0'
  100. username.style.fontFamily = 'sans-serif'
  101. username.style.fontSize = '14px'
  102. username.style.textDecoration = 'none'
  103. username.style.color = '#00376b'
  104. username.target = '_blank'
  105.  
  106. textInfo.appendChild(username)
  107. }
  108.  
  109. if (user.biography) {
  110. const bio = document.createElement('p')
  111.  
  112. bio.textContent = user.biography
  113. bio.style.margin = '0 0 10px 0'
  114. bio.style.whiteSpace = 'pre-line'
  115. bio.style.fontFamily = 'sans-serif'
  116. bio.style.fontSize = '14px'
  117.  
  118. textInfo.appendChild(bio)
  119. }
  120.  
  121. if (user.bioLinks && user.bioLinks.length > 0) {
  122. const links = document.createElement('div')
  123.  
  124. user.bioLinks.forEach(link => {
  125. const a = document.createElement('a')
  126. a.href = link.url
  127. a.textContent = link.title
  128. a.target = '_blank'
  129. a.style.display = 'block'
  130. a.style.fontFamily = 'sans-serif'
  131. a.style.fontSize = '14px'
  132. links.appendChild(a)
  133. })
  134.  
  135. textInfo.appendChild(links)
  136. }
  137.  
  138. info.appendChild(textInfo)
  139.  
  140. header.appendChild(info)
  141.  
  142. document.body.appendChild(header)
  143. }
  144. function renderMedia(mediaItems) {
  145. const mediaContainer = document.createElement('div')
  146. mediaContainer.style.display = 'grid'
  147. mediaContainer.style.gridTemplateColumns = 'repeat(auto-fill, minmax(320px, 1fr))'
  148. mediaContainer.style.gap = '20px'
  149. mediaContainer.style.padding = '20px'
  150.  
  151. mediaItems.forEach(item => {
  152. const mediaDiv = document.createElement('div')
  153. mediaDiv.className = 'media'
  154. mediaDiv.style.display = 'flex'
  155. mediaDiv.style.flexDirection = 'column'
  156. mediaDiv.style.alignItems = 'center'
  157.  
  158. if (item.isVideo) {
  159. const videoElement = document.createElement('video')
  160. videoElement.controls = true
  161. videoElement.width = 320
  162.  
  163. const source = document.createElement('source')
  164. source.src = item.videoUrl
  165. source.type = 'video/mp4'
  166.  
  167. videoElement.appendChild(source)
  168. mediaDiv.appendChild(videoElement)
  169. } else {
  170. const imageElement = document.createElement('img')
  171. imageElement.src = item.imageUrl
  172. imageElement.width = 320
  173. imageElement.style.height = 'auto'
  174. mediaDiv.appendChild(imageElement)
  175. }
  176.  
  177. const dateContainer = document.createElement('div')
  178. dateContainer.style.display = 'flex'
  179. dateContainer.style.alignItems = 'center'
  180. dateContainer.style.justifyContent = 'center'
  181. dateContainer.style.gap = '10px'
  182. dateContainer.style.width = '320px'
  183.  
  184. const date = document.createElement('p')
  185. date.textContent = item.date
  186. date.style.fontFamily = 'sans-serif'
  187. date.style.fontSize = '12px'
  188. date.style.margin = '5px 0'
  189.  
  190. dateContainer.appendChild(date)
  191.  
  192. if (item.shortcode) {
  193. const postLink = document.createElement('a')
  194. postLink.href = `/p/${item.shortcode}`
  195. postLink.textContent = '[post]'
  196. postLink.style.fontFamily = 'sans-serif'
  197. postLink.style.fontSize = '12px'
  198. postLink.style.color = 'blue'
  199. postLink.style.textDecoration = 'none'
  200. dateContainer.appendChild(postLink)
  201. }
  202.  
  203. if (item.isVideo) {
  204. const previewLink = document.createElement('a')
  205. previewLink.href = item.imageUrl
  206. previewLink.textContent = '[preview]'
  207. previewLink.style.fontFamily = 'sans-serif'
  208. previewLink.style.fontSize = '12px'
  209. previewLink.style.color = 'blue'
  210. previewLink.style.textDecoration = 'none'
  211. dateContainer.appendChild(previewLink)
  212. }
  213.  
  214. mediaDiv.appendChild(dateContainer)
  215.  
  216. const title = document.createElement('p')
  217. title.textContent = item.title
  218. title.style.fontFamily = 'sans-serif'
  219. title.style.fontSize = '12px'
  220. title.style.width = '320px'
  221. title.style.textAlign = 'center'
  222.  
  223. mediaDiv.appendChild(title)
  224. mediaContainer.appendChild(mediaDiv)
  225. })
  226.  
  227. document.body.appendChild(mediaContainer)
  228. }
  229. async function loadSinglePost({ csrfToken, appId, shortcode, isDebug }) {
  230. const url = isDebug ?
  231. `http://localhost:8000/post_${shortcode}.json` :
  232. `https://www.instagram.com/graphql/query`
  233.  
  234. const resp = await fetch(url, {
  235. "method": "POST",
  236. "credentials": "include",
  237. "headers": {
  238. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:131.0) Gecko/20100101 Firefox/131.0",
  239. "Accept": "*/*",
  240. "Accept-Language": "en-US,en;q=0.5",
  241. "Content-Type": "application/x-www-form-urlencoded",
  242. "X-FB-Friendly-Name": "PolarisPostActionLoadPostQueryQuery",
  243. "X-CSRFToken": csrfToken,
  244. "X-IG-App-ID": appId,
  245. "Origin": "https://www.instagram.com",
  246. "Sec-Fetch-Dest": "empty",
  247. "Sec-Fetch-Mode": "cors",
  248. "Sec-Fetch-Site": "same-origin",
  249. },
  250. "body": new URLSearchParams({
  251. "av": "0",
  252. "hl": "en",
  253. "__d": "www",
  254. "__user": "0",
  255. "__a": "1",
  256. "__req": "a",
  257. "__hs": "20168.HYP:instagram_web_pkg.2.1...0",
  258. "dpr": "2",
  259. "__ccg": "EXCELLENT",
  260. "fb_api_caller_class": "RelayModern",
  261. "fb_api_req_friendly_name": "PolarisPostActionLoadPostQueryQuery",
  262. "variables": JSON.stringify({
  263. "shortcode": shortcode,
  264. "fetch_tagged_user_count": null,
  265. "hoisted_comment_id": null,
  266. "hoisted_reply_id": null
  267. }),
  268. "server_timestamps": "true",
  269. "doc_id": "8845758582119845",
  270. }).toString()
  271. })
  272.  
  273. const data = await resp.json()
  274. const media = data.data.xdt_shortcode_media
  275.  
  276. let mediaItems = []
  277.  
  278. if (media.__typename === 'XDTGraphImage') {
  279. mediaItems.push({
  280. date: new Date(media.taken_at_timestamp * 1000).toISOString().slice(0, 19).replace('T', ' '),
  281. title: media.edge_media_to_caption?.edges[0]?.node.text || "No title",
  282. isVideo: false,
  283. videoUrl: null,
  284. imageUrl: media.display_url
  285. })
  286. }
  287. else if (media.__typename === 'XDTGraphVideo') {
  288. mediaItems.push({
  289. date: new Date(media.taken_at_timestamp * 1000).toISOString().slice(0, 19).replace('T', ' '),
  290. title: media.edge_media_to_caption?.edges[0]?.node.text || "No title",
  291. isVideo: true,
  292. videoUrl: media.video_url,
  293. imageUrl: media.display_url
  294. })
  295. }
  296. else if (media.__typename === 'XDTGraphSidecar') {
  297. media.edge_sidecar_to_children.edges.forEach(edge => {
  298. const child = edge.node
  299. mediaItems.push({
  300. date: new Date(media.taken_at_timestamp * 1000).toISOString().slice(0, 19).replace('T', ' '),
  301. title: media.edge_media_to_caption.edges[0]?.node.text || "No title",
  302. isVideo: child.__typename === 'XDTGraphVideo',
  303. videoUrl: child.__typename === 'XDTGraphVideo' ? child.video_url : null,
  304. imageUrl: child.display_url
  305. })
  306. })
  307. }
  308.  
  309.  
  310. renderProfileHeader({
  311. username: media.owner.username,
  312. fullName: media.owner.full_name,
  313. profilePicUrl: media.owner.profile_pic_url,
  314. isVerified: media.owner.is_verified
  315. })
  316.  
  317. renderMedia(mediaItems)
  318. }
  319.  
  320. async function loadProfile({ csrfToken, appId, username, isDebug }) {
  321. const url = isDebug ?
  322. `http://localhost:8000/profile.json` :
  323. `https://www.instagram.com/api/v1/users/web_profile_info/?username=${username}&hl=en`
  324.  
  325. const resp = await fetch(url, {
  326. "credentials": "include",
  327. "headers": {
  328. "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0",
  329. "Accept": "*/*",
  330. "Accept-Language": "en,en-US;q=0.5",
  331. "X-CSRFToken": csrfToken,
  332. "X-IG-App-ID": appId,
  333. "X-IG-WWW-Claim": "0",
  334. "X-Requested-With": "XMLHttpRequest",
  335. "Alt-Used": "www.instagram.com",
  336. "Sec-Fetch-Dest": "empty",
  337. "Sec-Fetch-Mode": "cors",
  338. "Sec-Fetch-Site": "same-origin",
  339. "Pragma": "no-cache",
  340. "Cache-Control": "no-cache"
  341. },
  342. "referrer": `https://www.instagram.com/${username}/?hl=en`,
  343. "method": "GET",
  344. "mode": "cors"
  345. })
  346.  
  347. const data = await resp.json()
  348. const mediaNodes = [
  349. ...data.data.user.edge_felix_video_timeline.edges,
  350. ...data.data.user.edge_owner_to_timeline_media.edges
  351. ]
  352.  
  353. const mediaItems = mediaNodes.flatMap(edge => {
  354. const media = edge.node
  355. if (media.__typename === 'GraphSidecar' && media.edge_sidecar_to_children) {
  356. return media.edge_sidecar_to_children.edges.map(child => ({
  357. date: new Date(media.taken_at_timestamp * 1000).toISOString().slice(0, 19).replace('T', ' '),
  358. title: media.edge_media_to_caption.edges[0]?.node.text || "No title",
  359. isVideo: child.node.__typename === 'GraphVideo',
  360. videoUrl: child.node.__typename === 'GraphVideo' ? child.node.video_url : null,
  361. imageUrl: child.node.display_url,
  362. shortcode: child.node.shortcode
  363. }))
  364. } else {
  365. return [{
  366. date: new Date(media.taken_at_timestamp * 1000).toISOString().slice(0, 19).replace('T', ' '),
  367. title: media.edge_media_to_caption.edges[0]?.node.text || "No title",
  368. isVideo: media.is_video,
  369. videoUrl: media.is_video ? media.video_url : null,
  370. imageUrl: media.display_url,
  371. shortcode: media.shortcode
  372. }]
  373. }
  374. })
  375.  
  376. renderProfileHeader({
  377. fullName: data.data.user.full_name,
  378. biography: data.data.user.biography,
  379. profilePicUrl: data.data.user.profile_pic_url_hd,
  380. bioLinks: data.data.user.bio_links,
  381. isVerified: data.data.user.is_verified,
  382. })
  383.  
  384. renderMedia(mediaItems)
  385. }
  386.  
  387. function run(secrets) {
  388. // first, stop the page from loading
  389. window.stop()
  390.  
  391. document.head.innerHTML = ''
  392. document.body.innerHTML = ''
  393.  
  394. // and now execute our code
  395. const postID = window.location.pathname.match(/(?:p|reel)\/([^\/]*)/)
  396.  
  397. if (postID) {
  398. const shortcode = postID[1]
  399. console.log(`Loading post: ${shortcode}`)
  400. loadSinglePost({ shortcode, ...secrets })
  401. } else {
  402. const username = window.location.pathname.split('/')[1]
  403. console.log(`Loading profile: ${username}`)
  404. loadProfile({ username, ...secrets })
  405. }
  406. }
  407.  
  408. (function () {
  409. 'use strict'
  410.  
  411. const isDebug = window.location.href.includes('localhost:8000')
  412. if (isDebug) {
  413. console.log("Debug mode enabled")
  414. document.body.innerHTML = ""
  415.  
  416. const shortcode = window.location.pathname.split('/').pop()
  417. if (shortcode) {
  418. loadSinglePost({ isDebug, shortcode })
  419. } else {
  420. loadProfile({ isDebug })
  421. }
  422.  
  423. return
  424. }
  425.  
  426. // we try to extract the secrets and run the app right away,
  427. // sometimes it works :)
  428. const secrets = extractSecretsAndRemoveScripts(1)
  429. if (!secrets) {
  430. // but since the user-script injection is kinda unpredictable
  431. // especially across different browsers and extensions,
  432. // we also fallback to a DOMContentLoaded event listener
  433. document.addEventListener('DOMContentLoaded', function () {
  434. window.stop() // we know that the secrets are in the DOM, so we can stop loading all other garbage
  435.  
  436. const secrets = extractSecretsAndRemoveScripts(2)
  437. if (!secrets) {
  438. console.log("Failed to extract secrets")
  439. return
  440. }
  441.  
  442. run(secrets)
  443. })
  444.  
  445. return
  446. }
  447.  
  448. run(secrets)
  449. })()