Back to Home
Gistify Snippet
kyiov
Sep 4·1 file(s)
Public

Scraper instagram

Scraper instagram

#github-gist#javascript#gist:dc79900c0c757316c05e05b5dbf98a01#gistsync:2026-09-02T12:40:35Z
0
Raw
instagram.jsjavascript
498 lines
1
2
3import * as __ns_637 from 'axios'
4const axios = __ns_637.default
5import * as __ns_638 from 'cheerio'
6const cheerio = __ns_638
7import * as __ns_639 from 'node:fs'
8const fs = __ns_639.default
9import * as __ns_640 from 'node:path'
10const path = __ns_640.default
11import * as __ns_641 from 'node:url'
12const pathToFileURL = __ns_641.pathToFileURL
13const fileURLToPath = __ns_641.fileURLToPath
14
15const BASE_URL = 'https://www.save-free.com'
16const AJAX_URL = `${BASE_URL}/wp-admin/admin-ajax.php`
17
18async function scrapeSaveFreeProfile(usernameOrUrl, options = {}) {
19 const { cfToken = '', lang = 'en' } = options
20
21 const cleanInput = String(usernameOrUrl || '')
22 .trim()
23 .replace(/^https?:\/\/(www\.)?instagram\.com\//i, '')
24 .replace(/\/.*$/, '')
25 .replace(/^@/, '')
26
27 if (!cleanInput) {
28 throw new Error('Username atau URL Instagram tidak valid.')
29 }
30
31 const formData = new URLSearchParams()
32 formData.append('action', 'save_profile')
33 formData.append('url', cleanInput)
34 formData.append('lang', lang)
35 if (cfToken) {
36 formData.append('cf_token', cfToken)
37 formData.append('cf-turnstile-response', cfToken)
38 }
39
40 const headers = {
41 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
42 'Referer': `${BASE_URL}/en/profile-downloader-2/`,
43 'Origin': BASE_URL,
44 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
45 'X-Requested-With': 'XMLHttpRequest',
46 'Accept': '*/*'
47 }
48
49 const response = await axios.post(AJAX_URL, formData.toString(), {
50 headers,
51 timeout: 30000,
52 validateStatus: () => true
53 })
54
55 if (response.status === 403 || (typeof response.data === 'string' && response.data.includes('Just a moment...'))) {
56 return {
57 status: false,
58 code: 403,
59 error: 'Cloudflare Turnstile WAF Protection Aktif. Membutuhkan CF token atau solver.',
60 cfRequired: true,
61 endpoint: AJAX_URL,
62 payload: Object.fromEntries(formData.entries())
63 }
64 }
65
66 const rawData = response.data
67 const $ = cheerio.load(typeof rawData === 'string' ? rawData : (rawData.html || rawData.data || ''))
68
69 const profilePic = $('img.ig_img, .profile-pic img, .avatar img, .result-card img').attr('src') || $('a.btn-download').attr('href') || null
70 const hdProfilePic = $('a.download_image_HD, a.btn-hd, a[download]').attr('href') || profilePic
71 const name = $('.profile-name, .user-name, h3').first().text().trim() || cleanInput
72 const username = $('.profile-username, .user-username').first().text().trim() || cleanInput
73 const bio = $('.profile-bio, .user-bio').first().text().trim() || '-'
74 const posts = $('.profile-posts, .posts-count').first().text().trim() || '0'
75 const followers = $('.profile-followers, .followers-count').first().text().trim() || '0'
76 const following = $('.profile-following, .following-count').first().text().trim() || '0'
77
78 return {
79 status: true,
80 result: {
81 username,
82 name,
83 bio,
84 stats: {
85 posts,
86 followers,
87 following
88 },
89 avatar: profilePic,
90 avatarHd: hdProfilePic,
91 rawHtml: typeof rawData === 'string' ? rawData.slice(0, 1000) : rawData
92 }
93 }
94}
95
96async function scrapeSaveFreeStories(usernameOrUrl, options = {}) {
97 const { cfToken = '', lang = 'en' } = options
98
99 const cleanInput = String(usernameOrUrl || '')
100 .trim()
101 .replace(/^https?:\/\/(www\.)?instagram\.com\/stories\//i, '')
102 .replace(/^https?:\/\/(www\.)?instagram\.com\//i, '')
103 .replace(/\/.*$/, '')
104 .replace(/^@/, '')
105
106 if (!cleanInput) {
107 throw new Error('Username atau link Instagram Story tidak valid.')
108 }
109
110 const formData = new URLSearchParams()
111 formData.append('action', 'save_story')
112 formData.append('url', cleanInput)
113 formData.append('lang', lang)
114 if (cfToken) {
115 formData.append('cf_token', cfToken)
116 formData.append('cf-turnstile-response', cfToken)
117 }
118
119 const headers = {
120 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
121 'Referer': `${BASE_URL}/en/stories-downloader/`,
122 'Origin': BASE_URL,
123 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
124 'X-Requested-With': 'XMLHttpRequest',
125 'Accept': '*/*'
126 }
127
128 const response = await axios.post(AJAX_URL, formData.toString(), {
129 headers,
130 timeout: 30000,
131 validateStatus: () => true
132 })
133
134 if (response.status === 403 || (typeof response.data === 'string' && response.data.includes('Just a moment...'))) {
135 return {
136 status: false,
137 code: 403,
138 error: 'Cloudflare Turnstile WAF Protection Aktif.',
139 cfRequired: true,
140 endpoint: AJAX_URL,
141 payload: Object.fromEntries(formData.entries())
142 }
143 }
144
145 const rawData = response.data
146 const $ = cheerio.load(typeof rawData === 'string' ? rawData : (rawData.html || rawData.data || ''))
147
148 const stories = []
149 $('.media-box, .result-card, .story-item, .card').each((_, el) => {
150 const card = $(el)
151 const videoUrl = card.find('video[src], video source[src]').attr('src') || card.find('a[href*=".mp4"]').attr('href')
152 const imgUrl = card.find('img.ig_img, img[src]').attr('src') || card.find('a[href*=".jpg"], a[href*=".png"]').attr('href')
153 const dlLink = card.find('a.btn-download, a[download]').attr('href') || videoUrl || imgUrl
154
155 if (dlLink) {
156 stories.push({
157 type: videoUrl || dlLink.includes('.mp4') ? 'video' : 'image',
158 url: dlLink,
159 thumbnail: imgUrl || null
160 })
161 }
162 })
163
164 return {
165 status: stories.length > 0,
166 username: cleanInput,
167 total: stories.length,
168 stories
169 }
170}
171
172const __dflt_16 = {
173 scrapeSaveFreeProfile,
174 scrapeSaveFreeStories
175}
176
177const GOOGLEBOT_UA = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
178const FACEBOOK_BOT_UA = 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)'
179const DESKTOP_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
180const DEFAULT_HEADERS = {
181 'User-Agent': GOOGLEBOT_UA,
182 Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
183 'Accept-Language': 'en-US,en;q=0.9,id;q=0.8',
184}
185
186function formatNumber(num) {
187 if (num === null || num === undefined || Number.isNaN(Number(num))) return '0'
188 const n = Number(num)
189 if (n >= 1e9) return `${(n / 1e9).toFixed(1).replace(/\.0$/, '')}B`
190 if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, '')}M`
191 if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, '')}K`
192 return n.toLocaleString('id-ID')
193}
194
195function deepFind(root, predicate, maxResults = 10) {
196 const results = []
197 const visited = new Set()
198 function recurse(node, key, parent) {
199 if (!node || typeof node !== 'object' || visited.has(node) || results.length >= maxResults) return
200 visited.add(node)
201 try {
202 if (predicate(node, key, parent)) results.push(node)
203 } catch {}
204 if (Array.isArray(node)) node.forEach((item, index) => recurse(item, index, node))
205 else Object.keys(node).forEach((item) => recurse(node[item], item, node))
206 }
207 recurse(root, null, null)
208 return results
209}
210
211function normalizeInstagramUrl(rawUrl) {
212 if (!rawUrl || typeof rawUrl !== 'string') throw new Error('URL Instagram wajib disertakan!')
213 let cleanUrl = rawUrl.trim()
214 const matchUrl = cleanUrl.match(/https?:\/\/[^\s]+/)
215 if (matchUrl) cleanUrl = matchUrl[0]
216 return `${cleanUrl.split('?')[0].replace(/\/$/, '')}/`
217}
218
219async function extractInstagram(targetUrl, options = {}) {
220 const cleanUrl = normalizeInstagramUrl(targetUrl)
221 if (cleanUrl.includes('/stories/')) return extractInstagramStories(targetUrl, options)
222
223 const res = await axios.get(cleanUrl, {
224 headers: DEFAULT_HEADERS,
225 timeout: 15000,
226 validateStatus: () => true,
227 })
228 if (res.status === 404) throw new Error('Postingan Instagram tidak ditemukan atau akun diprivat.')
229
230 const $ = cheerio.load(res.data)
231 const ogTitle = $('meta[property="og:title"]').attr('content') || $('title').text() || ''
232 const ogDesc = $('meta[property="og:description"]').attr('content') || $('meta[name="description"]').attr('content') || ''
233 const ogImage = $('meta[property="og:image"]').attr('content') || null
234 const ogVideo = $('meta[property="og:video"]').attr('content') || $('meta[property="og:video:secure_url"]').attr('content') || null
235 const jsonBlocks = []
236
237 $('script').each((_, el) => {
238 const text = $(el).html() || ''
239 if (!/xdt_api__v1__media__shortcode__web_info|shortcode_media|video_versions|image_versions2|carousel_media/.test(text)) return
240 try {
241 jsonBlocks.push(JSON.parse(text))
242 } catch {}
243 })
244
245 let mediaItem = null
246 for (const block of jsonBlocks) {
247 const items = deepFind(block, (node) => node && typeof node === 'object' && (node.shortcode || node.video_versions || node.carousel_media || node.edge_sidecar_to_children || node.image_versions2))
248 if (items.length) {
249 mediaItem = items[0]
250 break
251 }
252 }
253
254 const author = {
255 username: mediaItem?.user?.username || ogTitle.match(/@([a-zA-Z0-9_.]+)/)?.[1] || 'instagram_user',
256 fullName: mediaItem?.user?.full_name || ogTitle.split('on Instagram')[0]?.replace(/^@/, '').trim() || 'Instagram User',
257 avatar: mediaItem?.user?.profile_pic_url || null,
258 verified: Boolean(mediaItem?.user?.is_verified),
259 }
260 let caption = mediaItem?.caption?.text || ''
261 if (!caption && ogTitle.includes(': "')) caption = ogTitle.match(/:\s*(?:"|“)([\s\S]*?)(?:"|”)$/i)?.[1] || ''
262 if (!caption && ogDesc) caption = ogDesc.match(/-\s*(.*)/)?.[1] || ''
263
264 let likes = mediaItem?.like_count || 0
265 let comments = mediaItem?.comment_count || 0
266 if (!likes && ogDesc) likes = ogDesc.match(/([\d.,]+[kmb]?)\s*likes/i)?.[1] || 0
267 if (!comments && ogDesc) comments = ogDesc.match(/([\d.,]+[kmb]?)\s*comments/i)?.[1] || 0
268
269 let mediaType = 'image'
270 let videoUrl = null
271 const images = []
272 if (Array.isArray(mediaItem?.carousel_media) && mediaItem.carousel_media.length) {
273 mediaType = 'carousel'
274 mediaItem.carousel_media.forEach((item) => {
275 const video = item.video_versions?.[0]?.url
276 const image = item.image_versions2?.candidates?.[0]?.url
277 if (video) images.push({ type: 'video', url: video, thumbnail: image || null })
278 else if (image) images.push({ type: 'image', url: image })
279 })
280 } else if (mediaItem?.edge_sidecar_to_children?.edges) {
281 mediaType = 'carousel'
282 mediaItem.edge_sidecar_to_children.edges.forEach(({ node }) => {
283 const video = node.video_url
284 const image = node.display_url
285 if (video) images.push({ type: 'video', url: video, thumbnail: image || null })
286 else if (image) images.push({ type: 'image', url: image })
287 })
288 }
289 if (mediaType !== 'carousel') {
290 videoUrl = mediaItem?.video_versions?.[0]?.url || mediaItem?.video_url || ogVideo || null
291 if (videoUrl || /\/(reel|tv)\//.test(cleanUrl)) mediaType = 'video'
292 }
293 if (mediaType !== 'carousel' && mediaType !== 'video') {
294 const image = mediaItem?.image_versions2?.candidates?.[0]?.url || mediaItem?.display_url || ogImage
295 if (image) images.push({ type: 'image', url: image })
296 }
297
298 return {
299 status: 'success', source: 'instagram_native_ssr', type: mediaType, url: cleanUrl,
300 shortcode: mediaItem?.code || mediaItem?.shortcode || cleanUrl.split('/')[4] || '', author,
301 caption: caption || '-', likes: typeof likes === 'number' ? formatNumber(likes) : likes,
302 comments: typeof comments === 'number' ? formatNumber(comments) : comments,
303 takenAt: mediaItem?.taken_at ? new Date(mediaItem.taken_at * 1000).toLocaleString('id-ID') : null,
304 thumbnail: mediaItem?.image_versions2?.candidates?.[0]?.url || ogImage || null,
305 videoUrl, images: images.length ? images : ogImage ? [{ type: 'image', url: ogImage }] : [],
306 }
307}
308
309async function stalkInstagram(target, options = {}) {
310 if (!target || typeof target !== 'string') throw new Error('Username atau URL profil Instagram wajib diisi!')
311 const username = target.trim().replace(/^@/, '').replace(/https?:\/\/(?:www\.)?instagram\.com\//i, '').split('/')[0].split('?')[0].trim()
312 if (!username) throw new Error('Gagal mengekstrak username Instagram dari input.')
313 const profileUrl = `https://www.instagram.com/${username}/`
314 const res = await axios.get(profileUrl, {
315 headers: { 'User-Agent': FACEBOOK_BOT_UA, 'Accept-Language': 'en-US,en;q=0.9' },
316 timeout: 15000,
317 validateStatus: () => true,
318 })
319 if (res.status === 404) throw new Error(`Akun Instagram @${username} tidak ditemukan.`)
320 const $ = cheerio.load(res.data)
321 const ogTitle = $('meta[property="og:title"]').attr('content') || ''
322 const ogDesc = $('meta[property="og:description"]').attr('content') || $('meta[name="description"]').attr('content') || ''
323 const nameMatch = ogTitle.match(/^(.*?)\s*\(@[a-zA-Z0-9_.]+\)/i)
324 const followers = ogDesc.match(/([\d.,]+[KkMmBb]?)\s*(?:Followers|Pengikut)/i)
325 const following = ogDesc.match(/([\d.,]+[KkMmBb]?)\s*(?:Following|Mengikuti)/i)
326 const posts = ogDesc.match(/([\d.,]+[KkMmBb]?)\s*(?:Posts|Postingan|Kiriman)/i)
327 const bio = ogDesc.match(/(?:on Instagram|Instagram):\s*(?:"|“)([\s\S]*?)(?:"|”)$/i)?.[1] || ogDesc.match(/-\s*([\s\S]*)/)?.[1] || '-'
328 return {
329 status: 'success', username, fullName: nameMatch?.[1]?.trim() || username, bio: bio.trim(),
330 followers: followers?.[1] || '0', following: following?.[1] || '0', totalPosts: posts?.[1] || '0',
331 isPrivate: res.data.includes('"is_private":true'), isVerified: res.data.includes('"is_verified":true') || ogTitle.includes('Verified'),
332 avatarUrl: $('meta[property="og:image"]').attr('content') || null, profileUrl,
333 ...(options.download ? { note: 'Avatar download tersedia melalui downloadMediaFile().' } : {}),
334 }
335}
336
337async function extractInstagramStories(target, options = {}) {
338 const username = target
339 .trim()
340 .replace(/^https?:\/\/(?:www\.)?instagram\.com\/stories\//i, '')
341 .replace(/^https?:\/\/(?:www\.)?instagram\.com\//i, '')
342 .replace(/\/.*$/, '')
343 .replace(/^@/, '')
344 .trim()
345 if (!username) throw new Error('Username atau URL Instagram Story tidak valid.')
346
347 let stories = []
348 let source = null
349
350 try {
351 const data = await scrapeSaveFreeStories(username, options)
352 if (Array.isArray(data?.stories) && data.stories.length) {
353 stories = data.stories
354 source = 'save_free'
355 }
356 } catch (e) {
357 if (options.debug) console.warn('[Instagram Story] save-free gagal:', e.message)
358 }
359
360 if (!stories.length) {
361 try {
362 const res = await axios.get(`https://api.ryzendesu.vip/api/downloader/igstory?user=${encodeURIComponent(username)}`, {
363 timeout: 20000,
364 validateStatus: () => true,
365 })
366 const apiData = res.data
367 if (apiData && Array.isArray(apiData.data)) {
368 stories = apiData.data.map((item) => ({
369 type: item.type === 'video' || String(item.url || '').includes('.mp4') ? 'video' : 'image',
370 url: item.url || item,
371 thumbnail: item.thumbnail || null,
372 }))
373 source = 'ryzendesu'
374 }
375 } catch (e) {
376 if (options.debug) console.warn('[Instagram Story] ryzendesu gagal:', e.message)
377 }
378 }
379
380 if (!stories.length) {
381 return {
382 status: false,
383 type: 'story',
384 username,
385 source,
386 stories: [],
387 total: 0,
388 message: `Tidak ada Story aktif untuk @${username} (akun private / story sudah kadaluarsa).`,
389 }
390 }
391
392 const profile = await stalkInstagram(username).catch(() => null)
393 return {
394 status: true,
395 type: 'story',
396 source,
397 username,
398 fullName: profile?.fullName || username,
399 avatarUrl: profile?.avatarUrl || null,
400 total: stories.length,
401 stories,
402 message: `Ditemukan ${stories.length} Story aktif untuk @${username}.`,
403 }
404}
405
406async function downloadMediaFile(url, outputPath) {
407 const dir = path.dirname(outputPath)
408 if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
409 const res = await axios.get(url, { responseType: 'arraybuffer', headers: { 'User-Agent': DESKTOP_UA, Referer: 'https://www.instagram.com/' }, timeout: 30000 })
410 fs.writeFileSync(outputPath, Buffer.from(res.data))
411 return outputPath
412}
413
414async function downloadInstagram(targetUrl, options = {}) {
415 const result = await extractInstagram(targetUrl, options)
416 const outDir = options.outputDir || path.join(process.cwd(), 'downloads', 'instagram')
417 const prefix = result.shortcode || result.username || Date.now()
418 const downloadedFiles = []
419 let files = []
420 if (result.type === 'story' && Array.isArray(result.stories)) {
421 files = result.stories.map((item, index) => ({ type: item.type, url: item.url, name: `${prefix}_story_${index + 1}.${item.type === 'video' ? 'mp4' : 'jpg'}` }))
422 } else if (result.type === 'video' && result.videoUrl) {
423 files = [{ type: 'video', url: result.videoUrl, name: `${prefix}_video.mp4` }]
424 } else if (Array.isArray(result.images)) {
425 files = result.images.map((item, index) => ({ type: item.type, url: item.url, name: `${prefix}_item_${index + 1}.${item.type === 'video' ? 'mp4' : 'jpg'}` }))
426 }
427 for (const file of files) downloadedFiles.push({ type: file.type, path: await downloadMediaFile(file.url, path.join(outDir, file.name)) })
428 return { ...result, downloadedFiles }
429}
430
431const instagram = extractInstagram
432const ig = extractInstagram
433const stalk = stalkInstagram
434
435const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))
436if (isMain) {
437 const [command, target, ...flags] = process.argv.slice(2)
438 if (!command || flags.includes('--help') || flags.includes('-h')) {
439 console.log('Pakai: node system/lib/scraper/instagram.js <URL> [--download] | stalk <username> | story <username>')
440 } else {
441 const shouldDownload = flags.includes('--download') || flags.includes('-d')
442 const run = command === 'stalk'
443 ? stalkInstagram(target)
444 : command === 'story' || command === 'stories'
445 ? shouldDownload
446 ? downloadInstagram(`https://www.instagram.com/stories/${target}/`)
447 : extractInstagramStories(target)
448 : shouldDownload
449 ? downloadInstagram(command)
450 : extractInstagram(command)
451 run.then((result) => console.log(JSON.stringify(result, null, 2))).catch((error) => {
452 console.error('[ERROR]', error.message)
453 process.exitCode = 1
454 })
455 }
456}
457
458if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
459 const __exports = { extractInstagram: extractInstagram, stalkInstagram: stalkInstagram, extractInstagramStories: extractInstagramStories, downloadMediaFile: downloadMediaFile, downloadInstagram: downloadInstagram, normalizeInstagramUrl: normalizeInstagramUrl, instagram: instagram, ig: ig, stalk: stalk }
460 const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function'))
461 const __names = Object.keys(__fns)
462 if (__names.length === 0) {
463 console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.')
464 } else {
465 const __search = __names.find((n) => /search|cari/i.test(n))
466 const __name = __search || __names[0]
467 const __fn = __fns[__name]
468 const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn))
469 const __defaultArgs = (() => {
470 const n = __name.toLowerCase()
471 if (/emoji/.test(n)) return [{ emoji: '😀😃' }]
472 if (/textpro|generate.*text|effect/.test(n)) return ['neon', 'Hello']
473 if (/image|wallpaper|search|query|anime|movie|manga|news|lyrics|wiki|quote|stalk|detail/.test(n)) return ['naruto']
474 if (/download|youtube|video|audio|music|spotify|tiktok|instagram|facebook|twitter|reels|terabox|media/.test(n)) return ['https://www.youtube.com/watch?v=dQw4w9WgXcQ']
475 if (/dork/.test(n)) return ['example.com', 'documents']
476 if (/tts|speech|prompt|chat|ask|translate/.test(n)) return ['Hello world']
477 if (/parse|format|normalize|list|features/.test(n)) return []
478 return ['test']
479 })()
480 let __args = __defaultArgs
481 if (process.argv.length > 2) __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } })
482 if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) {
483 console.log('Fungsi: ' + __names.join(', '))
484 console.log('Contoh: node ' + "instagram.js" + ' ' + __name + ' <arg>')
485 } else {
486 ;(async () => {
487 try {
488 const r = __isClass ? new __fn(...__args) : await __fn(...__args)
489 console.log(JSON.stringify(r, null, 2))
490 } catch (e) {
491 console.error('Error: ' + (e && e.message ? e.message : e))
492 process.exitCode = 1
493 }
494 })()
495 }
496 }
497}
498
Discussion & Comments0

No comments yet — be the first to leave feedback or start a code discussion.

Sign in with your GitHub account to write comments and join the discussion.