├── .eslintignore
├── src
├── pages
│ └── Home
│ │ ├── index.ts
│ │ ├── Home.module.scss
│ │ └── Home.tsx
├── components
│ ├── Card
│ │ ├── index.ts
│ │ ├── Card.module.scss
│ │ └── Card.tsx
│ ├── Input
│ │ ├── index.ts
│ │ ├── Input.module.scss
│ │ └── Input.tsx
│ ├── Tab
│ │ ├── index.ts
│ │ ├── Tab.module.scss
│ │ └── Tab.tsx
│ ├── Buttton
│ │ ├── index.ts
│ │ ├── Button.module.scss
│ │ └── Button.tsx
│ ├── HowToUse
│ │ ├── index.ts
│ │ ├── HowToUse.module.scss
│ │ └── HowToUse.tsx
│ ├── TextArea
│ │ ├── index.ts
│ │ ├── TextArea.module.scss
│ │ └── TextArea.tsx
│ ├── GithubCorner
│ │ ├── index.ts
│ │ └── GithubCorner.tsx
│ ├── ResultProcessor
│ │ ├── index.ts
│ │ ├── ResultProcessor.module.scss
│ │ └── ResultProcessor.tsx
│ └── Galllery
│ │ ├── Gallery.module.scss
│ │ └── Gallery.tsx
├── hooks
│ ├── index.ts
│ └── useCopyToClipboard.ts
├── app
│ ├── favicon.ico
│ ├── page.tsx
│ ├── google-captcha-wrapper.tsx
│ ├── api
│ │ ├── proxy-image
│ │ │ └── route.ts
│ │ ├── get-profile-picture
│ │ │ └── route.ts
│ │ └── get-user-id
│ │ │ └── route.ts
│ └── layout.tsx
├── assets
│ ├── images
│ │ ├── how-to-use-1.jpg
│ │ ├── how-to-use-2.jpg
│ │ ├── how-to-use-3.jpg
│ │ └── how-to-use-4.jpg
│ ├── icons
│ │ ├── icon-x.svg
│ │ ├── icon-copy.svg
│ │ ├── icon-video.svg
│ │ ├── icon-image.svg
│ │ └── icon-video-preview.svg
│ └── styles
│ │ └── globals.scss
├── utils
│ └── sleep.ts
├── services
│ ├── fetch-proxy-image.ts
│ ├── verify-recaptcha.ts
│ ├── get-user-info.ts
│ ├── get-profile-picture.ts
│ └── get-user-id.ts
└── constants
│ ├── regexes.ts
│ └── urls.ts
├── .dockerignore
├── .prettierignore
├── commitlint.config.js
├── .husky
├── pre-commit
└── commit-msg
├── .prettierrc
├── .env.example
├── docker-compose.yaml
├── Dockerfile
├── .gitignore
├── public
├── vercel.svg
└── next.svg
├── tsconfig.json
├── LICENSE
├── next.config.js
├── .eslintrc.json
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/*.json
--------------------------------------------------------------------------------
/src/pages/Home/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Home';
2 |
--------------------------------------------------------------------------------
/src/components/Card/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Card';
2 |
--------------------------------------------------------------------------------
/src/components/Input/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Input';
2 |
--------------------------------------------------------------------------------
/src/components/Tab/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Tab';
2 |
--------------------------------------------------------------------------------
/src/components/Buttton/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Button';
2 |
--------------------------------------------------------------------------------
/src/components/HowToUse/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './HowToUse';
2 |
--------------------------------------------------------------------------------
/src/components/TextArea/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './TextArea';
2 |
--------------------------------------------------------------------------------
/src/components/GithubCorner/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './GithubCorner';
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 |
2 | .git
3 | .results
4 | *Dockerfile*
5 | node_modules
6 | .env
7 | .next
--------------------------------------------------------------------------------
/src/components/ResultProcessor/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ResultProcessor';
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 |
2 | **/.next/**
3 | **/dist/**
4 | **/tmp/**
5 | *.json
6 | *.md
7 | *.yaml
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useCopyToClipboard } from './useCopyToClipboard';
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run lint
5 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx --no -- commitlint --edit
5 |
--------------------------------------------------------------------------------
/src/assets/images/how-to-use-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-1.jpg
--------------------------------------------------------------------------------
/src/assets/images/how-to-use-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-2.jpg
--------------------------------------------------------------------------------
/src/assets/images/how-to-use-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-3.jpg
--------------------------------------------------------------------------------
/src/assets/images/how-to-use-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasinatesim/instagram-media-downloader/HEAD/src/assets/images/how-to-use-4.jpg
--------------------------------------------------------------------------------
/src/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | const MS = 2000;
2 | export function sleep() {
3 | return new Promise((resolve) => setTimeout(resolve, MS));
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": true,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "printWidth": 120,
7 | "trailingComma": "es5"
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Tab/Tab.module.scss:
--------------------------------------------------------------------------------
1 | /* Tab.module.scss */
2 | .buttons {
3 | display: flex;
4 | gap: 12px;
5 | }
6 |
7 | .active {
8 | background-color: #007bff;
9 | color: #fff;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/HowToUse/HowToUse.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | img {
3 | width: 100%;
4 | max-width: 100%;
5 | display: block;
6 | border: 1px solid #222;
7 | border-radius: 4px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/ResultProcessor/ResultProcessor.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-top: 30px;
3 | padding-top: 20px;
4 | border-top: 1px solid #eee;
5 | }
6 |
7 | .description {
8 | margin-bottom: 12px;
9 | line-height: 1.4;
10 | }
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # You need Google Recaptcha token for below field and you should add *localhost* domain in Google Recaptcha console "Domains" section
2 | # https://www.google.com/recaptcha/admin/create
3 | NEXT_PUBLIC_RECAPTCHA_SITE_KEY=
4 | RECAPTCHA_SECRET_KEY=
5 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build:
5 | context: ./
6 | tty: true
7 | stdin_open: true
8 | command: npm run dev
9 | volumes:
10 | - ./:/app
11 | - /app/node_modules
12 | ports:
13 | - '3000:3000'
14 |
--------------------------------------------------------------------------------
/src/components/TextArea/TextArea.module.scss:
--------------------------------------------------------------------------------
1 | .textarea {
2 | width: 100%;
3 | height: 150px;
4 | padding: 8px;
5 | font-size: 16px;
6 | box-sizing: border-box;
7 | border: 1px solid #ccc;
8 | border-radius: 4px;
9 | outline: none;
10 | margin-bottom: 16px;
11 | }
12 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # base image
2 | FROM node:18.18.0
3 |
4 | # set working directory
5 | WORKDIR /app
6 |
7 | # copy application files to the container
8 | COPY . .
9 |
10 | # install dependencies
11 | RUN npm install
12 |
13 | # expose the port 3000
14 | EXPOSE 3000
15 |
16 | # start the application
17 | CMD ["npm", "run", "dev"]
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import GoogleCaptchaWrapper from './google-captcha-wrapper';
3 |
4 | import HomePage from '@/pages/Home';
5 |
6 | const Home = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default Home;
15 |
--------------------------------------------------------------------------------
/src/components/Buttton/Button.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | border: 0;
3 | padding: 10px;
4 | margin-bottom: 5px;
5 | cursor: pointer;
6 | border-radius: 5px;
7 | }
8 |
9 | .secondary {
10 | color: #fff;
11 | background-color: #3498db;
12 | &:disabled {
13 | background-color: #96d5ff;
14 | cursor: not-allowed;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/services/fetch-proxy-image.ts:
--------------------------------------------------------------------------------
1 | const fetchProxyImage = async (imageUrl: string) => {
2 | const response = await fetch(`/api/proxy-image?imageUrl=${encodeURIComponent(imageUrl)}`);
3 | const data = await response.json();
4 |
5 | if (data.imageUrlBase64) {
6 | return data.imageUrlBase64;
7 | }
8 | };
9 |
10 | export default fetchProxyImage;
11 |
--------------------------------------------------------------------------------
/src/assets/styles/globals.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 | .container {
7 | max-width: 750px;
8 | margin: auto;
9 | padding: 8px;
10 |
11 | @media (min-width: 768px) {
12 | padding: 20px;
13 | }
14 | }
15 |
16 | a {
17 | color: inherit;
18 | }
19 |
20 | .grecaptcha-badge {
21 | visibility: hidden;
22 | z-index: 1010;
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-video.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/constants/regexes.ts:
--------------------------------------------------------------------------------
1 | export const INSTAGRAM_USERNAME_REGEX_FOR_STORIES = /instagram\.com\/stories\/([^/]+)\/?(?:\d+\/?)?$/;
2 | export const INSTAGRAM_USERNAME_REGEX_FOR_PROFILE = /\/([^\/]+)\/?$/;
3 | export const INSTAGRAM_HIGHLIGHT_ID_REGEX = /\/stories\/highlights\/(\d+)\/?$/;
4 | export const INSTAGRAM_POSTPAGE_REGEX = /p/;
5 | export const INSTAGRAM_REELSPAGE_REGEX = /reel/;
6 | export const INSTAGRAM_HIGHLIGHTSPAGE_REGEX = /stories\/highlights/;
7 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-image.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/google-captcha-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
4 |
5 | export default function GoogleCaptchaWrapper({ children }: { children: React.ReactNode }) {
6 | const recaptchaKey: string | undefined = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
7 | return (
8 |
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-video-preview.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.module.scss:
--------------------------------------------------------------------------------
1 | .description {
2 | margin-bottom: 12px;
3 | color: #555;
4 | }
5 |
6 | .recentItemsContainer {
7 | margin: 20px 0;
8 | user-select: none;
9 | }
10 |
11 | .recentItemsSummary {
12 | font-size: 1.2rem;
13 | font-weight: bold;
14 | cursor: pointer;
15 | }
16 |
17 | .categoryContainer {
18 | margin-bottom: 20px;
19 | }
20 |
21 | .categoryTitle {
22 | font-size: 18px;
23 | margin-bottom: 10px;
24 | }
25 |
26 | .localStorageItems {
27 | display: flex;
28 | flex-wrap: wrap;
29 | gap: 10px;
30 | }
31 |
32 | .itemContainer {
33 | display: flex;
34 | align-items: center;
35 | margin-right: 8px;
36 | }
37 |
38 | .deleteIcon {
39 | width: 20px;
40 | height: 20px;
41 | cursor: pointer;
42 | margin-left: 4px;
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/api/proxy-image/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from 'next/server';
2 |
3 | export const dynamic = 'force-dynamic';
4 |
5 | export async function GET(request: NextRequest) {
6 | try {
7 | const { searchParams } = new URL(request.url);
8 | const imageUrl = searchParams.get('imageUrl');
9 |
10 | const response = await fetch(imageUrl as string);
11 |
12 | const arrayBuffer = await response.arrayBuffer();
13 |
14 | const base64Image = Buffer.from(arrayBuffer).toString('base64');
15 | const imageUrlBase64 = `data:image/png;base64,${base64Image}`;
16 |
17 | const data = {
18 | imageUrlBase64,
19 | };
20 |
21 | return Response.json(data, { status: 200 });
22 | } catch (error) {
23 | return Response.json(error, { status: 400 });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/services/verify-recaptcha.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const RECAPTCHA_THRESHOLD = 0.5;
4 |
5 | async function verifyRecaptcha(token: string) {
6 | const secretKey = process.env.RECAPTCHA_SECRET_KEY;
7 | const verificationUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${token}`;
8 |
9 | try {
10 | const response = await axios.post(verificationUrl, {}, { headers: { 'Content-Type': 'application/json' } });
11 |
12 | if (!response.data.success || response.data.score < RECAPTCHA_THRESHOLD) {
13 | throw new Error('Recaptcha verification failed');
14 | }
15 |
16 | return response.data;
17 | } catch (error) {
18 | console.error('Recaptcha verification error:', (error as Error).message);
19 | throw error;
20 | }
21 | }
22 |
23 | export default verifyRecaptcha;
24 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
3 | import { Toaster } from 'react-hot-toast';
4 |
5 | import type { Metadata } from 'next';
6 | import { Inter } from 'next/font/google';
7 |
8 | import GithubCorner from '@/components/GithubCorner';
9 |
10 | import '@/assets/styles/globals.scss';
11 |
12 | const inter = Inter({ subsets: ['latin'] });
13 |
14 | export const metadata: Metadata = {
15 | title: 'Instagram Media Downloader',
16 | description: 'Generated by create next app',
17 | };
18 |
19 | export default function RootLayout({ children }: { children: React.ReactNode }) {
20 | return (
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Input/Input.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | margin-bottom: 16px;
5 | }
6 |
7 | .input {
8 | padding: 8px;
9 | font-size: 16px;
10 | width: 100%;
11 | box-sizing: border-box;
12 | border: 1px solid #ccc;
13 | border-radius: 4px;
14 | outline: none;
15 | }
16 |
17 | .input:disabled {
18 | background-color: #f5f5f5;
19 | color: #777;
20 | }
21 |
22 | .copyButton {
23 | cursor: pointer;
24 | background-color: #3498db;
25 | color: #ffffff;
26 | border: none;
27 | padding: 8px;
28 | margin-left: 5px;
29 | border-radius: 4px;
30 | display: flex;
31 | align-items: center;
32 | justify-content: center;
33 | transition: background-color 0.3s ease;
34 |
35 | &:hover {
36 | background-color: #2980b9;
37 | }
38 |
39 | &:disabled {
40 | background-color: #96d5ff;
41 | cursor: not-allowed;
42 | }
43 |
44 | svg {
45 | font-size: 20px;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import IconCopy from '@/assets/icons/icon-copy.svg';
4 |
5 | import styles from './Input.module.scss';
6 |
7 | type Props = {
8 | placeholder: string;
9 | value: string;
10 | readOnly?: boolean;
11 | disabled?: boolean;
12 | onCopy?: () => void;
13 | onBlur?: () => void;
14 | onChange?: (event: React.ChangeEvent) => void;
15 | };
16 |
17 | const Input: React.FC = ({ placeholder, value, onChange, onCopy, disabled = false, ...props }) => {
18 | return (
19 |
20 |
29 | {onCopy && (
30 |
31 |
32 |
33 | )}
34 |
35 | );
36 | };
37 |
38 | export default Input;
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Yasin ATEŞ
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Galllery/Gallery.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | max-width: 750px;
3 | margin: auto;
4 | padding: 20px;
5 | }
6 |
7 | .icon {
8 | display: flex;
9 | align-items: center;
10 | font-size: 18px;
11 | margin-right: 12px;
12 | }
13 |
14 | .description {
15 | display: flex;
16 | align-items: center;
17 | line-height: 1.4;
18 | border-bottom: 1px solid #ddd;
19 | padding-top: 4px;
20 | padding-bottom: 4px;
21 |
22 | &:last-of-type {
23 | margin-bottom: 12px;
24 | }
25 |
26 | svg {
27 | margin-left: 12px;
28 | }
29 |
30 | strong {
31 | margin-left: 4px;
32 | margin-right: 4px;
33 | }
34 | }
35 |
36 | .additionalResults {
37 | margin-top: 40px;
38 | padding-top: 20px;
39 | border-top: 1px solid #eee;
40 | }
41 |
42 | .additionalResults h3 {
43 | margin-bottom: 20px;
44 | padding-bottom: 10px;
45 | border-bottom: 1px solid #ddd;
46 | }
47 |
48 | .additionalResult {
49 | margin-bottom: 30px;
50 | padding-bottom: 20px;
51 | border-bottom: 1px dashed #eee;
52 | }
53 |
54 | .additionalResult:last-child {
55 | border-bottom: none;
56 | margin-bottom: 0;
57 | padding-bottom: 0;
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/Buttton/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import cx from 'classnames';
4 |
5 | import css from './Button.module.scss';
6 |
7 | type Classnames = {
8 | container?: string;
9 | };
10 |
11 | type Props = {
12 | classnames?: Classnames;
13 | type?: 'button' | 'submit' | 'reset' | undefined;
14 | size?: 'medium' | 'large';
15 | shape?: 'circle';
16 | variant?: 'primary' | 'secondary';
17 | width?: number | string;
18 | };
19 |
20 | const Button: React.FC, 'size'>> = ({
21 | children,
22 | classnames,
23 | size = 'medium',
24 | style,
25 | shape,
26 | variant = 'primary',
27 | type = 'button',
28 | width,
29 | ...props
30 | }) => {
31 | const cn: Classnames = {
32 | container: cx(css.container, css[variant], css[size], shape && css[shape], classnames?.container),
33 | };
34 |
35 | return (
36 |
44 | {children}
45 |
46 | );
47 | };
48 |
49 | export default Button;
50 |
--------------------------------------------------------------------------------
/src/hooks/useCopyToClipboard.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useCopyToClipboard = () => {
4 | const [isCopied, setIsCopied] = useState(false);
5 |
6 | const copyToClipboard = (text: string) => {
7 | if (navigator.clipboard) {
8 | navigator.clipboard.writeText(text).then(
9 | () => {
10 | setIsCopied(true);
11 | },
12 | (err) => {
13 | console.error('Error copying to clipboard:', err);
14 | setIsCopied(false);
15 | }
16 | );
17 | } else {
18 | // Fallback for browsers that do not support the Clipboard API
19 | const textArea = document.createElement('textarea');
20 | textArea.value = text;
21 | document.body.appendChild(textArea);
22 | textArea.select();
23 |
24 | try {
25 | document.execCommand('copy');
26 | setIsCopied(true);
27 | } catch (err) {
28 | console.error('Error copying to clipboard:', err);
29 | setIsCopied(false);
30 | }
31 |
32 | document.body.removeChild(textArea);
33 | }
34 | };
35 |
36 | return { isCopied, copyToClipboard };
37 | };
38 |
39 | export default useCopyToClipboard;
40 |
--------------------------------------------------------------------------------
/src/components/Card/Card.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | }
4 |
5 | .gallery {
6 | border-bottom: 1px solid #ddd;
7 | margin-bottom: 4px;
8 |
9 | &:last-of-type {
10 | border-bottom: 0;
11 | }
12 | }
13 |
14 | .index {
15 | font-size: 18px;
16 | margin-right: 12px;
17 | display: none;
18 | min-width: 30px;
19 |
20 | @media (min-width: 768px) {
21 | display: inline;
22 | }
23 | }
24 |
25 | .image,
26 | .video {
27 | position: relative;
28 | display: inline-block;
29 | }
30 |
31 | .hasVideo {
32 | @media (min-width: 768px) {
33 | margin-left: 6px;
34 | }
35 | }
36 |
37 | .icon {
38 | position: absolute;
39 | top: 5px;
40 | right: 5px;
41 | color: #b7dde9;
42 | font-size: 22px;
43 | }
44 |
45 | .loader {
46 | margin-top: 12px;
47 | margin-bottom: 12px;
48 | width: 48px;
49 | height: 48px;
50 | border: 5px solid #3498db;
51 | border-bottom-color: transparent;
52 | border-radius: 50%;
53 | display: inline-block;
54 | box-sizing: border-box;
55 | animation: rotation 1s linear infinite;
56 | }
57 |
58 | @keyframes rotation {
59 | 0% {
60 | transform: rotate(0deg);
61 | }
62 | 100% {
63 | transform: rotate(360deg);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | async rewrites() {
4 | return [
5 | {
6 | source: '/api/get-user-info',
7 | destination: 'https://www.instagram.com/:username',
8 | },
9 | ];
10 | },
11 | webpack(config) {
12 | // Grab the existing rule that handles SVG imports
13 | const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg'));
14 |
15 | config.module.rules.push(
16 | // Reapply the existing rule, but only for svg imports ending in ?url
17 | {
18 | ...fileLoaderRule,
19 | test: /\.svg$/i,
20 | resourceQuery: /url/, // *.svg?url
21 | },
22 | // Convert all other *.svg imports to React components
23 | {
24 | test: /\.svg$/i,
25 | issuer: fileLoaderRule.issuer,
26 | resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
27 | use: ['@svgr/webpack'],
28 | }
29 | );
30 |
31 | // Modify the file loader rule to ignore *.svg, since we have it handled now.
32 | fileLoaderRule.exclude = /\.svg$/i;
33 |
34 | return config;
35 | },
36 | };
37 |
38 | module.exports = nextConfig;
39 |
--------------------------------------------------------------------------------
/src/components/HowToUse/HowToUse.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import howToUseImage1 from '@/assets/images/how-to-use-1.jpg';
4 | import howToUseImage2 from '@/assets/images/how-to-use-2.jpg';
5 | import howToUseImage3 from '@/assets/images/how-to-use-3.jpg';
6 | import howToUseImage4 from '@/assets/images/how-to-use-4.jpg';
7 |
8 | import styles from './HowToUse.module.scss';
9 |
10 | type Props = {};
11 |
12 | const HowToUse = (props: Props) => {
13 | return (
14 |
15 |
How To Use
16 |
17 |
18 | Copy Instagram url
19 |
20 |
21 |
22 | Paste your url first input in page
23 |
24 |
25 |
26 | Select and copy all JSON code in the tab
27 |
28 |
29 |
30 | Paste the code in the input. You will then be redirected to result page
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default HowToUse;
39 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "plugin:prettier/recommended"],
3 | "plugins": ["simple-import-sort"],
4 | "rules": {
5 | "import/no-anonymous-default-export": "off",
6 | "@next/next/no-img-element": "off",
7 | "react-hooks/exhaustive-deps": "off",
8 | "simple-import-sort/imports": "error",
9 | "simple-import-sort/exports": "error"
10 | },
11 | "overrides": [
12 | {
13 | "files": ["*.ts", "*.tsx"],
14 | "rules": {
15 | "simple-import-sort/imports": [
16 | "error",
17 | {
18 | "groups": [
19 | ["^react"],
20 | ["^next"],
21 | ["^@?\\w"],
22 | ["^(@/constants)(/.*|$)"],
23 | ["^(@/data)(/.*|$)"],
24 | ["^(@/hooks)(/.*|$)"],
25 | ["^(@/assets/icons)(/.*|$)"],
26 | ["^(@/assets/images)(/.*|$)"],
27 | ["^(@/services)(/.*|$)"],
28 | ["^(@/store)(/.*|$)"],
29 | ["^(@/components)(/.*|$)"],
30 | ["^\\u0000"],
31 | ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
32 | ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
33 | ["^.+\\.?(scss)$"]
34 | ]
35 | }
36 | ]
37 | }
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/services/get-user-info.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import getUserId from './get-user-id';
4 |
5 | // https://i.instagram.com/api/v1/users/${id}/info/
6 | export async function getUserInfo(username: string) {
7 | const userId = await getUserId(username);
8 |
9 | try {
10 | const url = `https://i.instagram.com/api/v1/users/${userId}/info/`;
11 | const headers = {
12 | Accept: 'application/json, text/plain, */*',
13 | 'User-Agent':
14 | 'Mozilla/5.0 (Linux; Android 9; GM1903 Build/PKQ1.190110.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3770.143 Mobile Safari/537.36 Instagram 103.1.0.15.119 Android (28/9; 420dpi; 1080x2260; OnePlus; GM1903; OnePlus7; qcom; sv_SE; 164094539)',
15 | 'sec-fetch-dest': 'empty',
16 | 'sec-fetch-mode': 'cors',
17 | 'sec-fetch-site': 'same-origin',
18 | };
19 |
20 | const response = await axios.get(url, { headers, maxBodyLength: Infinity, maxRedirects: 0 });
21 |
22 | if (response.status !== 200) {
23 | throw new Error(`Request failed with status: ${response.status}`);
24 | }
25 |
26 | return response.data.user;
27 | } catch (error) {
28 | console.error('Instagram API request failed. Falling back to alternative method');
29 |
30 | const errorMessage = error instanceof Error ? error.message : 'Error retrieving Instagram user ID';
31 | throw new Error(errorMessage);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/api/get-profile-picture/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from 'next/server';
2 |
3 | import getProfilePicture from '@/services/get-profile-picture';
4 | import verifyRecaptcha, { RECAPTCHA_THRESHOLD } from '@/services/verify-recaptcha';
5 |
6 | import { sleep } from '@/utils/sleep';
7 |
8 | export async function POST(request: NextRequest) {
9 | const { username, token } = await request.json();
10 |
11 | if (typeof username !== 'string') {
12 | throw new Error('Invalid username format');
13 | }
14 |
15 | try {
16 | const recaptchaResponse = await verifyRecaptcha(token);
17 |
18 | if (recaptchaResponse.success && recaptchaResponse.score >= RECAPTCHA_THRESHOLD) {
19 | await sleep();
20 |
21 | try {
22 | const url = await getProfilePicture(username);
23 |
24 | if (url) {
25 | return new Response(
26 | JSON.stringify({
27 | url,
28 | }),
29 | { status: 200 }
30 | );
31 | }
32 | } catch (error) {
33 | const errorMessage = error instanceof Error ? error.message : 'Something went wrong';
34 | const errorResponse = { status: 'Failed', message: errorMessage };
35 |
36 | return new Response(JSON.stringify({ error: errorResponse }), { status: 400 });
37 | }
38 | }
39 | } catch (error) {
40 | return new Response(JSON.stringify({ error: (error as Error).message }), { status: 400 });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Tab/Tab.tsx:
--------------------------------------------------------------------------------
1 | // Tab.tsx
2 | import React, { ReactNode, useState } from 'react';
3 |
4 | import cx from 'classnames';
5 |
6 | import Button from '@/components/Buttton';
7 |
8 | import styles from './Tab.module.scss';
9 |
10 | interface TabProps {
11 | children: ReactNode;
12 | }
13 |
14 | interface ContentProps {
15 | tab: ReactNode | string;
16 | children: ReactNode;
17 | }
18 |
19 | const Tab: React.FC & { Content: React.FC } = ({ children }) => {
20 | const [activeIndex, setActiveIndex] = useState(0);
21 |
22 | const contents = React.Children.toArray(children).filter((c: any) => c.type === Tab.Content);
23 |
24 | const activeContent = contents[activeIndex];
25 |
26 | return (
27 | <>
28 |
29 | {contents.map((content, key) => (
30 |
31 | setActiveIndex(key)}
33 | classnames={{
34 | container: cx({
35 | [styles.active]: activeIndex === key,
36 | }),
37 | }}
38 | >
39 | {/* @ts-ignore */}
40 | {content.props.tab}
41 |
42 |
43 | ))}
44 |
45 |
46 | {activeContent}
47 | >
48 | );
49 | };
50 |
51 | const Content: React.FC = ({ children }) => {
52 | return {children}
;
53 | };
54 |
55 | Tab.Content = Content;
56 |
57 | export default Tab;
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "instagram-media-downloader",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/yasinatesim/instagram-media-downloader"
8 | },
9 | "engines": {
10 | "node": "18"
11 | },
12 | "scripts": {
13 | "dev": "next dev",
14 | "build": "next build",
15 | "start": "next start",
16 | "lint": "next lint --fix && tsc&& prettier --check --ignore-path .prettierignore .",
17 | "format": "prettier --write --ignore-unknown .",
18 | "prepare": "husky install"
19 | },
20 | "dependencies": {
21 | "axios": "^1.6.7",
22 | "cheerio": "1.0.0-rc.12",
23 | "classnames": "^2.5.1",
24 | "firebase-admin": "^12.0.0",
25 | "instagram-private-api": "^1.45.3",
26 | "next": "14.0.4",
27 | "node-fetch": "^3.3.2",
28 | "react": "^18",
29 | "react-dom": "^18",
30 | "react-google-recaptcha-v3": "^1.10.1",
31 | "react-hot-toast": "^2.4.1",
32 | "sass": "^1.69.7"
33 | },
34 | "devDependencies": {
35 | "@commitlint/cli": "^18.4.4",
36 | "@commitlint/config-conventional": "^18.4.4",
37 | "@svgr/webpack": "^8.1.0",
38 | "@types/classnames": "^2.3.1",
39 | "@types/node": "^20",
40 | "@types/react": "^18",
41 | "@types/react-dom": "^18",
42 | "eslint": "^8",
43 | "eslint-config-next": "14.0.4",
44 | "eslint-config-prettier": "^9.1.0",
45 | "eslint-plugin-prettier": "^5.1.3",
46 | "eslint-plugin-simple-import-sort": "^10.0.0",
47 | "husky": "^8.0.3",
48 | "lint-staged": "^15.2.0",
49 | "prettier": "^3.2.1",
50 | "typescript": "^5"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/api/get-user-id/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from 'next/server';
2 |
3 | import getUserId from '@/services/get-user-id';
4 | import verifyRecaptcha, { RECAPTCHA_THRESHOLD } from '@/services/verify-recaptcha';
5 |
6 | import { sleep } from '@/utils/sleep';
7 |
8 | // Todo: Implement this -- new service -- for user posts
9 | // https://www.instagram.com/graphql/query/?query_id=17888483320059182&id={user_id}&first=24
10 | // or
11 | //https://www.instagram.com/graphql/query/?query_hash=e769aa130647d2354c40ea6a439bfc08&variables={"id":{user_id},"first": 24}
12 |
13 | export async function POST(request: NextRequest) {
14 | const { username, token } = await request.json();
15 |
16 | if (typeof username !== 'string') {
17 | throw new Error('Invalid username format');
18 | }
19 |
20 | try {
21 | const recaptchaResponse = await verifyRecaptcha(token);
22 |
23 | if (recaptchaResponse.success && recaptchaResponse.score >= RECAPTCHA_THRESHOLD) {
24 | await sleep();
25 |
26 | try {
27 | const userId = await getUserId(username);
28 |
29 | return new Response(
30 | JSON.stringify({
31 | userId,
32 | }),
33 | { status: 200 }
34 | );
35 | } catch (error) {
36 | const errorMessage = error instanceof Error ? error.message : 'Something went wrong';
37 | const errorResponse = { status: 'Failed', message: errorMessage };
38 |
39 | return new Response(JSON.stringify({ error: errorResponse }), { status: 400 });
40 | }
41 | }
42 | } catch (error) {
43 | return new Response(JSON.stringify({ error: (error as Error).message }), { status: 400 });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/constants/urls.ts:
--------------------------------------------------------------------------------
1 | export const INSTAGRAM_PROFILE_URL = `https://www.instagram.com/`;
2 | export const INSTAGRAM_GRAPHQL_URL_FOR_STORIES = `https://www.instagram.com/graphql/query/?query_hash=de8017ee0a7c9c45ec4260733d81ea31&variables={"reel_ids":[], "highlight_reel_ids":[],"precomposed_overlay":false}`;
3 | export const INSTAGRAM_GRAPHQL_URL_FOR_HIGHLIGHTS = `https://www.instagram.com/graphql/query/?query_hash=de8017ee0a7c9c45ec4260733d81ea31&variables={"reel_ids":[],"tag_names":[],"location_ids":[],"highlight_reel_ids":[""],"precomposed_overlay":false,"show_story_viewer_list":true,"story_viewer_fetch_count":50,"story_viewer_cursor":""}`;
4 | export const INSTAGRAM_GRAPHQL_URL_FOR_POST = `https://www.instagram.com/graphql/query/?doc_id=8845758582119845&variables={\"shortcode\":\"\",\"fetch_tagged_user_count\":null,\"hoisted_comment_id\":null,\"hoisted_reply_id\":null}`;
5 | export const INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS = `https://www.instagram.com/graphql/query/?doc_id=8759034877476257&variables={"data":{"count":12,"include_relationship_info":true,"latest_besties_reel_media":true,"latest_reel_media":true},"username":"","__relay_internal__pv__PolarisIsLoggedInrelayprovider":true,"__relay_internal__pv__PolarisFeedShareMenurelayprovider":true}`;
6 | export const INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS_WITH_AFTER = `https://www.instagram.com/graphql/query/?doc_id=8759034877476257&variables={"after":"","before":null,"data":{"count":12,"include_relationship_info":true,"latest_besties_reel_media":true,"latest_reel_media":true},"username":"","__relay_internal__pv__PolarisIsLoggedInrelayprovider":true,"__relay_internal__pv__PolarisFeedShareMenurelayprovider":true}`;
7 |
--------------------------------------------------------------------------------
/src/components/TextArea/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 |
3 | import styles from './TextArea.module.scss';
4 |
5 | type Props = {
6 | placeholder: string;
7 | value: string;
8 | onChange?: (event: React.ChangeEvent) => void;
9 | onFileUpload?: (content: string) => void;
10 | };
11 |
12 | const Textarea: React.FC = ({ placeholder, value, onChange, onFileUpload, ...props }) => {
13 | const fileInputRef = useRef(null);
14 |
15 | const handleFileChange = (event: React.ChangeEvent) => {
16 | const file = event.target.files?.[0];
17 | if (!file) return;
18 |
19 | const reader = new FileReader();
20 | reader.onload = (e) => {
21 | const content = e.target?.result as string;
22 | if (onFileUpload) {
23 | onFileUpload(content);
24 | }
25 | };
26 | reader.readAsText(file);
27 | };
28 |
29 | const triggerFileInput = () => {
30 | fileInputRef.current?.click();
31 | };
32 |
33 | return (
34 |
35 |
36 |
37 |
51 | Upload JSON File
52 |
53 |
54 | );
55 | };
56 |
57 | export default Textarea;
58 |
--------------------------------------------------------------------------------
/src/components/Card/Card.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | import cx from 'classnames';
4 |
5 | import IconImage from '@/assets/icons/icon-image.svg';
6 | import IconVideo from '@/assets/icons/icon-video.svg';
7 | import IconVideoPreview from '@/assets/icons/icon-video-preview.svg';
8 |
9 | import fetchProxyImage from '@/services/fetch-proxy-image';
10 |
11 | import styles from './Card.module.scss';
12 |
13 | type Props = {
14 | index?: string | number;
15 | imageUrl: string;
16 | hasVideo: boolean;
17 | videoUrl: string;
18 | };
19 |
20 | const Card: React.FC = ({ index, imageUrl, hasVideo, videoUrl }) => {
21 | const [proxyImageUrl, setProxyImageUrl] = useState(null);
22 |
23 | const handleFetchProxyImage = async () => {
24 | const data = await fetchProxyImage(imageUrl);
25 | setProxyImageUrl(data);
26 | };
27 |
28 | useEffect(() => {
29 | handleFetchProxyImage();
30 | }, [imageUrl]);
31 |
32 | return (
33 |
63 | );
64 | };
65 |
66 | export default Card;
67 |
--------------------------------------------------------------------------------
/src/components/GithubCorner/GithubCorner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import packageJson from '../../../package.json';
4 |
5 | const GithubCorner = () => {
6 | return (
7 | <>
8 |
9 |
23 |
24 |
30 |
35 |
36 |
37 |
43 | >
44 | );
45 | };
46 |
47 | export default GithubCorner;
48 |
--------------------------------------------------------------------------------
/src/components/ResultProcessor/ResultProcessor.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import toast from 'react-hot-toast';
3 |
4 | import { INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS_WITH_AFTER } from '@/constants/urls';
5 |
6 | import { useCopyToClipboard } from '@/hooks';
7 |
8 | import Input from '@/components/Input';
9 | import TextArea from '@/components/TextArea';
10 |
11 | import styles from './ResultProcessor.module.scss';
12 |
13 | type Props = {
14 | onJsonProcessed: (jsonData: string) => void;
15 | endCursor?: string;
16 | username?: string;
17 | };
18 |
19 | const ResultProcessor: React.FC = ({ onJsonProcessed, endCursor, username }) => {
20 | const { copyToClipboard } = useCopyToClipboard();
21 | const [generatedUrl, setGeneratedUrl] = useState('');
22 | const [jsonInput, setJsonInput] = useState('');
23 |
24 | useEffect(() => {
25 | if (endCursor && username) {
26 | const url = INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS_WITH_AFTER.replace('', username).replace(
27 | '',
28 | endCursor
29 | );
30 | setGeneratedUrl(url);
31 | }
32 | }, [endCursor, username]);
33 |
34 | const handleJsonProcessing = (value: string) => {
35 | setJsonInput(value);
36 |
37 | try {
38 | JSON.parse(value);
39 | onJsonProcessed(value);
40 | setGeneratedUrl('');
41 | setJsonInput('');
42 | toast.success('JSON processed successfully!');
43 | } catch (e) {
44 | if (value.trim() !== '') {
45 | toast.error('Invalid JSON');
46 | }
47 | }
48 | };
49 |
50 | const handleJsonPaste = (event: React.ChangeEvent) => {
51 | const value = event.target.value;
52 | handleJsonProcessing(value);
53 | };
54 |
55 | return (
56 |
57 |
58 | Copy the generated link and open it in a new tab. Then copy the JSON output from the tab
59 |
60 |
setGeneratedUrl(e.target.value)}
64 | onCopy={() => copyToClipboard(generatedUrl)}
65 | />
66 |
67 |
68 | Paste the JSON data into the input below. You will then be redirected to result page 🎉
69 |
70 |
77 | );
78 | };
79 |
80 | export default ResultProcessor;
81 |
--------------------------------------------------------------------------------
/src/services/get-profile-picture.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import { getUserInfo } from '@/services/get-user-info';
4 |
5 | import getUserId from './get-user-id';
6 |
7 | async function getProfilePictureFromInstagrmApi(username: string) {
8 | const userId = await getUserId(username);
9 |
10 | try {
11 | const response = await axios.post(
12 | 'https://www.instagram.com/graphql/query',
13 | `__d=www&__user=0&__a=1&__req=1o&fb_api_caller_class=RelayModern&fb_api_req_friendly_name=PolarisUserHoverCardContentV2Query&variables=%7B%22userID%22%3A%22${userId}%22%2C%22username%22%3A%22${username}%22%7D&server_timestamps=true&doc_id=7953815318003879`,
14 | {
15 | headers: {
16 | accept: '*/*',
17 | 'accept-language': 'en-US,en;q=0.9,tr;q=0.8,es;q=0.7',
18 | 'cache-control': 'no-cache',
19 | 'content-type': 'application/x-www-form-urlencoded',
20 | dpr: '1',
21 | pragma: 'no-cache',
22 | priority: 'u=1, i',
23 | 'sec-ch-prefers-color-scheme': 'dark',
24 | 'sec-ch-ua': '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"',
25 | 'sec-ch-ua-full-version-list':
26 | '"Not A(Brand";v="99.0.0.0", "Google Chrome";v="121.0.6167.184", "Chromium";v="121.0.6167.184"',
27 | 'sec-ch-ua-mobile': '?0',
28 | 'sec-ch-ua-model': '""',
29 | 'sec-ch-ua-platform': '"macOS"',
30 | 'sec-ch-ua-platform-version': '"13.4.1"',
31 | 'sec-fetch-dest': 'empty',
32 | 'sec-fetch-mode': 'cors',
33 | 'sec-fetch-site': 'same-origin',
34 | 'viewport-width': '1920',
35 | 'x-asbd-id': '918374',
36 | 'x-bloks-version-id': 'c6c7b90c445758d2f27d257e0b850f3b5e7b27dfbb81b6a470d6a40cb040a25a',
37 | 'x-csrftoken': 'yNmAkP3tQl4eV7sHcDoRfW9gZsX1A2bE',
38 | 'x-fb-friendly-name': 'PolarisUserHoverCardContentV2DirectQuery',
39 | 'x-fb-lsd': 'pLxQbR4a9HgWu1sVyFnCdM3eLxP9sV7j',
40 | 'x-ig-app-id': '672145893218637',
41 | },
42 | }
43 | );
44 |
45 | const pic = response?.data?.data?.user?.hd_profile_pic_url_info?.url;
46 |
47 | if (pic) {
48 | return pic;
49 | }
50 |
51 | if (response.status !== 200) {
52 | throw new Error('Network response was not ok');
53 | }
54 | } catch (error) {
55 | console.log('error:', error);
56 | throw new Error('User not found in Instagram Api response');
57 | }
58 | }
59 |
60 | async function getProfilePicture(username: string) {
61 | try {
62 | return await getProfilePictureFromInstagrmApi(username);
63 | } catch (instagramApiError) {
64 | console.log('Instagram API request failed. Trying the last alternative method...', instagramApiError);
65 | try {
66 | const data = await getUserInfo(username);
67 | if (data?.hd_profile_pic_url_info?.url) {
68 | return data.hd_profile_pic_url_info.url;
69 | } else {
70 | throw new Error('User not found in user info response');
71 | }
72 | } catch (lastError) {
73 | const userId = await getUserId(username);
74 | const generatedUrl = `https://www.instagram.com/graphql/query/?doc_id=9539110062771438&variables={\"id\":\"${userId}\",\"render_surface\":\"PROFILE\"}`;
75 | const manualInputMessage = {
76 | message:
77 | 'Automatic profile picture could not be retrieved. Please copy the JSON data from the Instagram page and paste it below.',
78 | url: generatedUrl,
79 | instructions: 'Copy the JSON data from the opened page and paste it below.',
80 | };
81 |
82 | throw new Error(JSON.stringify(manualInputMessage));
83 | }
84 | }
85 | }
86 |
87 | export default getProfilePicture;
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Instagram Media Downloader
6 |
7 |
8 |
9 |
10 |
11 |
12 | Effortlessly download Instagram reels, stories, and posts without the need for user authentication. Enjoy seamless content saving in a user-friendly app.
13 |
14 |
15 |
16 | · View App
17 |
18 |
19 | ## 📖 About
20 |
21 | **Instagram Media Downloader:**
22 |
23 | Effortlessly download Instagram reels, stories, and posts with our user-friendly app. No login needed! 🚀
24 |
25 | **Features:**
26 | - **Profile Picture:** Instantly download full size Instagram profile pictures.
27 | - **Story Download:** Easily save your favorite stories.
28 | - **Highlights Download:** Save Instagram highlights effortlessly.
29 | - **Post Download:** Download Instagram posts hassle-free.
30 | - **Reels Download:** One-click download for Instagram reels.
31 |
32 | Simplify your media saving experience! No login, no hassle. Enjoy Instagram content offline. 📲✨
33 |
34 |
35 |
36 | ### 📚Tech Stack
37 |
38 |
39 |
40 |
41 | dilame/instagram-private-api
42 | NodeJS Instagram private API SDK. Written in TypeScript.
43 |
44 |
45 |
46 |
47 |
48 | Next.js
49 |
50 |
51 |
52 | The React Framework for SEO Friendly website and more...
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Commitlint
61 |
62 |
63 |
64 | Send commit messages to conventional commits rules
65 |
66 |
67 |
68 |
69 | TypeScript
70 | TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
71 |
72 |
73 |
74 |
75 |
76 |
77 | CSS Modules
78 |
79 |
80 |
81 | Class names and animation names are scoped locally CSS files
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | SASS
90 |
91 |
92 |
93 | The most mature, stable, and powerful professional grade CSS extension language in the world
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Editorconfig
102 |
103 |
104 |
105 | Helps maintain consistent coding styles for my working on the same project across various editors and IDEs
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | Eslint
114 |
115 |
116 |
117 | Find and fix problems in your JavaScript code
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | Eslint Simple Import Sort
128 |
129 |
130 |
131 | Enforce consistent import order in your JavaScript code
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | Prettier
140 |
141 |
142 |
143 | An opinionated code formatter
144 |
145 |
146 |
147 |
148 |
149 | ## 🧐 What's inside?
150 |
151 | ### Instagram Story, Highlight, Reels and Post Downloader
152 |
153 | 1. Copy Instagram URL
154 |
155 | 
156 |
157 | 2. Paste the URL into the first input on the page
158 |
159 | 
160 |
161 | 3. Select and copy all JSON code from the tab
162 |
163 | 
164 |
165 | 4. Paste the code into the input. You will then be redirected to the result page
166 |
167 | 
168 |
169 |
170 |
171 | ### Instagram Full Size Profile Picture Downloader
172 | Allows you to easily retrieve and download high-resolution profile pictures from Instagram. Whether you want to save your own profile picture or download someone else's, this tool simplifies the process, providing you with a hassle-free way to access and store full-size profile images. Enjoy the convenience of quickly obtaining Instagram profile pictures for various purposes with this efficient downloader.
173 |
174 | ## Getting Started
175 |
176 | ### 📦 Prerequisites
177 |
178 | - Node (v18.17.0+)
179 |
180 | - Npm (v9.0.0+)
181 |
182 | ### ⚙️ How To Use
183 |
184 |
185 | 1. Clone this repository
186 |
187 | ```bash
188 |
189 | git clone https://github.com/yasinatesim/instagram-media-downloader.git
190 |
191 | ```
192 |
193 |
194 |
195 | 2. Add .env file on root
196 |
197 | ```bash
198 |
199 | # You need Google Recaptcha token for below field and you should add *localhost* domain in Google Recaptcha console "Domains" section
200 | # https://www.google.com/recaptcha/admin/create
201 | NEXT_PUBLIC_RECAPTCHA_SITE_KEY=
202 | RECAPTCHA_SECRET_KEY=
203 |
204 | ```
205 |
206 |
207 |
208 | 3. Install the project dependencies
209 |
210 | ```bash
211 |
212 | npm install
213 |
214 | ```
215 |
216 | **For Development**
217 |
218 | ```bash
219 |
220 | npm run dev
221 |
222 | ```
223 |
224 | ### For Docker
225 |
226 | Docker Build
227 |
228 | ```bash
229 | docker build -t instagram-media-downloader .
230 | ```
231 |
232 | Docker Run
233 |
234 | ```bash
235 | docker run -p 3000:3000 -d instagram-media-downloader
236 | ```
237 |
238 | App is running to [http://localhost:3000/](http://localhost:3000/)
239 |
240 | ### For Docker Compose
241 |
242 | ```bash
243 | docker-compose up --build
244 | ```
245 |
246 | App is running to [http://localhost:3000/](http://localhost:3000/)
247 |
248 | **For Production Build & Build Start**
249 |
250 | ```bash
251 |
252 | npm run build
253 |
254 | ```
255 |
256 | and
257 |
258 | ```bash
259 |
260 | npm run start
261 |
262 | ```
263 |
264 | **For Lint**
265 |
266 | ```bash
267 | npm run lint
268 | ```
269 |
270 |
271 | ## 🔑 License
272 |
273 | * Copyright © 2024 - MIT License.
274 |
275 | See [LICENSE](https://github.com/yasinatesim/instagram-media-downloader/blob/master/LICENSE) for more information.
276 |
277 | ---
278 |
279 | _This README was generated with by [markdown-manager](https://github.com/yasinatesim/markdown-manager)_ 🥲
280 |
--------------------------------------------------------------------------------
/src/services/get-user-id.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import * as cheerio from 'cheerio';
3 |
4 | async function fetchData(url: string) {
5 | const headers = {
6 | Accept: 'application/json, text/plain, */*',
7 | 'User-Agent':
8 | 'Mozilla/5.0 (Linux; Android 9; GM1903 Build/PKQ1.190110.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3770.143 Mobile Safari/537.36 Instagram 103.1.0.15.119 Android (28/9; 420dpi; 1080x2260; OnePlus; GM1903; OnePlus7; qcom; sv_SE; 164094539)',
9 | 'sec-fetch-dest': 'empty',
10 | 'sec-fetch-mode': 'cors',
11 | 'sec-fetch-site': 'same-origin',
12 | };
13 |
14 | const response = await axios.get(url, { headers, maxBodyLength: Infinity, maxRedirects: 0 });
15 |
16 | if (response.status !== 200) {
17 | throw new Error(`Request to ${url} failed with status: ${response.status}`);
18 | }
19 |
20 | return response.data;
21 | }
22 |
23 | async function getUserIdFromInstagramSearch(username: string) {
24 | const url = `https://i.instagram.com/api/v1/users/search/?q=${username.toLocaleUpperCase()}&count=30&timezone_offset=${String(new Date().getTimezoneOffset() * -60)}`;
25 | const result = await fetchData(url);
26 |
27 | const users = result.users;
28 | const account = users.find((user: { username: string }) => user.username === username);
29 | if (!account) {
30 | throw new Error('Instagram Account not found in Instagram Search API response');
31 | }
32 | return String(account.pk);
33 | }
34 |
35 | async function getUserIdFromTopSearch(username: string) {
36 | const url = `https://www.instagram.com/web/search/topsearch/?context=blended&query=${username}&rank_token=0.3953592318270893&count=1`;
37 | const result = await fetchData(url);
38 |
39 | const users = result.users;
40 | const account = users.find((item: { user: { username: string } }) => item.user.username === username);
41 | if (!account) {
42 | throw new Error('Instagram Account not found in TopSearch API response');
43 | }
44 | return String(account.pk);
45 | }
46 |
47 | async function getUserIdFromWebProfile(username: string) {
48 | const url = `https://i.instagram.com/api/v1/users/web_profile_info/?username=${username}`;
49 | const result = await fetchData(url);
50 |
51 | if (result && result.data && result.data.user) {
52 | return result.data.user.id;
53 | }
54 |
55 | throw new Error('User not found in WebProfile API response');
56 | }
57 |
58 | async function getUserIdFromProfilePage(username: string) {
59 | const url = `https://www.instagram.com/${username}/`;
60 |
61 | try {
62 | const response = await axios(url);
63 | const $ = cheerio.load(response.data);
64 |
65 | const script = $('script')
66 | // @ts-ignore
67 | .filter((i, el) => $(el).html().includes('profilePage_'))
68 | .html();
69 | // @ts-ignore
70 | const user_id_start = script.indexOf('"profilePage_') + '"profilePage_'.length;
71 | // @ts-ignore
72 | const user_id_end = script.indexOf('"', user_id_start);
73 | // @ts-ignore
74 | const user_id = script.substring(user_id_start, user_id_end);
75 |
76 | return user_id;
77 | } catch (error) {
78 | throw new Error('User not found in Profile Page response');
79 | }
80 | }
81 |
82 | async function getUserIdFromInstagramGraphQL(username: string): Promise {
83 | try {
84 | const response = await axios.post(
85 | 'https://www.instagram.com/graphql/query',
86 | {
87 | av: '69334875141353716',
88 | __d: 'www',
89 | variables: JSON.stringify({
90 | data: {
91 | context: 'blended',
92 | include_reel: 'true',
93 | query: username.trim(),
94 | rank_token: '',
95 | search_surface: 'web_top_search',
96 | },
97 | hasQuery: true,
98 | }),
99 | doc_id: '9153895011291216',
100 | },
101 | {
102 | headers: {
103 | 'X-IG-App-ID': '672145893218637',
104 | 'X-CSRFToken': 'yNmAkP3tQl4eV7sHcDoRfW9gZsX1A2bE',
105 | 'Content-Type': 'application/x-www-form-urlencoded',
106 | 'X-FB-Friendly-Name': 'PolarisSearchBoxRefetchableQuery',
107 | 'User-Agent':
108 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
109 | },
110 | }
111 | );
112 |
113 | if (response.status !== 200) {
114 | throw new Error('Network response was not ok');
115 | }
116 |
117 | // Assuming the response structure matches the search results
118 | // You might need to adjust this path based on the actual response structure
119 | const users = response?.data?.data?.xdt_api__v1__fbsearch__topsearch_connection?.users;
120 | const targetUser = users?.find((item: any) => item.user.username === username.trim()).user;
121 |
122 | if (!targetUser?.id) {
123 | throw new Error('User ID not found in response');
124 | }
125 |
126 | return targetUser.id;
127 | } catch (error) {
128 | console.error('Error fetching user ID:', error);
129 | throw new Error('Failed to fetch Instagram user ID');
130 | }
131 | }
132 |
133 | async function getUserId(username: string) {
134 | try {
135 | return await getUserIdFromProfilePage(username);
136 | } catch (profilePageError) {
137 | console.log('profilePage request failed. Trying alternative methods...', profilePageError);
138 |
139 | try {
140 | return await getUserIdFromInstagramGraphQL(username);
141 | } catch (getUserIdFromInstagramGraphQL) {
142 | console.log(
143 | 'getUserIdFromInstagramGraphQL API request failed. Trying alternative methods...',
144 | getUserIdFromInstagramGraphQL
145 | );
146 | try {
147 | return await getUserIdFromWebProfile(username);
148 | } catch (webProfileError) {
149 | console.log('WebProfile API request failed. Trying alternative methods...', webProfileError);
150 | try {
151 | return await getUserIdFromTopSearch(username);
152 | } catch (topSearchError) {
153 | console.log('TopSearch API request failed. Trying alternative methods...', topSearchError);
154 | try {
155 | return await getUserIdFromInstagramSearch(username);
156 | } catch (lastError) {
157 | console.log('Instagram Search API request failed. Trying the last alternative method...', lastError);
158 | lastError instanceof Error ? lastError.message : 'Error retrieving Instagram user ID';
159 | throw new Error(lastError instanceof Error ? lastError.message : 'Error retrieving Instagram user ID');
160 | }
161 | }
162 | }
163 | }
164 | }
165 | }
166 |
167 | export default getUserId;
168 |
--------------------------------------------------------------------------------
/src/components/Galllery/Gallery.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import IconImage from '@/assets/icons/icon-image.svg';
4 | import IconVideo from '@/assets/icons/icon-video.svg';
5 | import IconVideoPreview from '@/assets/icons/icon-video-preview.svg';
6 |
7 | import Card from '@/components/Card';
8 | import ResultProcessor from '@/components/ResultProcessor';
9 |
10 | import styles from './Gallery.module.scss';
11 |
12 | type Props = {
13 | result: any;
14 | isAdditionalResult?: boolean;
15 | endCursor?: string;
16 | username?: string;
17 | };
18 |
19 | const AdditionalResults: React.FC<{ results: any[] }> = ({ results }) => {
20 | if (!results.length) return null;
21 |
22 | return (
23 |
24 |
Additional Results
25 | {results.map((result, index) => {
26 | if (result?.data?.xdt_shortcode_media) {
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | if (result?.data?.xdt_api__v1__feed__user_timeline_graphql_connection?.edges) {
35 | const edges = result.data.xdt_api__v1__feed__user_timeline_graphql_connection.edges;
36 | const items = edges.map((edge: any) => edge.node);
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 | );
49 | })}
50 |
51 | );
52 | };
53 |
54 | const Gallery = ({ result, isAdditionalResult = false, endCursor, username }: Props) => {
55 | const [additionalResults, setAdditionalResults] = useState([]);
56 |
57 | const handleJsonProcessed = (jsonData: string) => {
58 | try {
59 | const parsedData = JSON.parse(jsonData);
60 | setAdditionalResults((prev) => [...prev, parsedData]);
61 | } catch (e) {
62 | console.error('Failed to parse JSON', e);
63 | }
64 | };
65 |
66 | if (isAdditionalResult) {
67 | if (result?.data?.xdt_shortcode_media) {
68 | const wrappedResult = { items: [result.data.xdt_shortcode_media] };
69 | return renderGalleryContent(wrappedResult, true, undefined, undefined, endCursor);
70 | }
71 |
72 | if (result?.data?.xdt_api__v1__feed__user_timeline_graphql_connection?.edges) {
73 | const edges = result.data.xdt_api__v1__feed__user_timeline_graphql_connection.edges;
74 | const items = edges.map((edge: any) => edge.node);
75 | return renderGalleryContent({ items }, true, undefined, undefined, endCursor);
76 | }
77 |
78 | return renderGalleryContent(result, true, undefined, undefined, endCursor);
79 | }
80 |
81 | return renderGalleryContent(result, false, handleJsonProcessed, additionalResults, endCursor, username);
82 | };
83 |
84 | const renderGalleryContent = (
85 | result: any,
86 | isAdditionalResult: boolean,
87 | handleJsonProcessed?: (jsonData: string) => void,
88 | additionalResults?: any[],
89 | endCursor?: string,
90 | username?: string
91 | ) => {
92 | if (result?.data?.reels_media) {
93 | return (
94 |
95 | {!isAdditionalResult &&
}
96 | {result?.data?.reels_media?.[0].items.map((item: any, key: string) => (
97 |
104 | ))}
105 | {!isAdditionalResult && additionalResults &&
}
106 | {!isAdditionalResult && handleJsonProcessed && (
107 |
108 | )}
109 |
110 | );
111 | }
112 |
113 | // Instagram GraphQL Post (xdt_shortcode_media) and Sidecar (multi-image/video) support
114 | if (result?.items?.[0]?.__typename === 'XDTGraphImage' || result?.items?.[0]?.__typename === 'XDTGraphVideo') {
115 | const post = result.items[0];
116 | const imageUrl = post.display_url || post.thumbnail_src;
117 | const hasVideo = !!post.video_url;
118 | const videoUrl = post.video_url || '';
119 | return (
120 |
121 | {!isAdditionalResult &&
}
122 |
123 | {!isAdditionalResult && additionalResults &&
}
124 | {!isAdditionalResult && handleJsonProcessed && (
125 |
126 | )}
127 |
128 | );
129 | }
130 |
131 | if (result?.items?.[0]?.__typename === 'XDTGraphSidecar' && result?.items?.[0]?.edge_sidecar_to_children?.edges) {
132 | const edges = result.items[0].edge_sidecar_to_children.edges;
133 | return (
134 |
135 | {!isAdditionalResult &&
}
136 | {edges.map((edge: any, key: number) => {
137 | const node = edge.node;
138 | const imageUrl = node.display_url || node.thumbnail_src;
139 | const hasVideo = !!node.video_url;
140 | const videoUrl = node.video_url || '';
141 | return (
142 |
149 | );
150 | })}
151 | {!isAdditionalResult && additionalResults &&
}
152 | {!isAdditionalResult && handleJsonProcessed && (
153 |
154 | )}
155 |
156 | );
157 | }
158 |
159 | if (result?.items?.[0]?.__typename === 'XDTMediaDict' || result?.items?.[0]?.media_type) {
160 | return (
161 |
162 | {!isAdditionalResult &&
}
163 | {result.items.map((item: any, key: number) => {
164 | // Carousel post ise
165 | if (item.media_type === 8 && item.carousel_media) {
166 | return item.carousel_media.map((carouselItem: any, ckey: number) => (
167 |
174 | ));
175 | }
176 |
177 | if (item.media_type === 2 && item.video_versions) {
178 | return (
179 |
186 | );
187 | }
188 |
189 | return (
190 |
197 | );
198 | })}
199 | {!isAdditionalResult && additionalResults &&
}
200 | {!isAdditionalResult && handleJsonProcessed && (
201 |
202 | )}
203 |
204 | );
205 | }
206 |
207 | if (result?.items?.[0]?.carousel_media) {
208 | return (
209 |
210 | {!isAdditionalResult &&
}
211 | {result.items?.[0].carousel_media.map((item: any, key: string) => (
212 |
219 | ))}
220 | {!isAdditionalResult && additionalResults &&
}
221 | {!isAdditionalResult && handleJsonProcessed && (
222 |
223 | )}
224 |
225 | );
226 | } else {
227 | return (
228 |
229 | {!isAdditionalResult &&
}
230 |
235 | {!isAdditionalResult && additionalResults &&
}
236 | {!isAdditionalResult && handleJsonProcessed && (
237 |
238 | )}
239 |
240 | );
241 | }
242 | };
243 |
244 | const Description: React.FC = () => {
245 | return (
246 |
247 |
248 | *
249 |
250 |
251 |
252 |
253 | for post and story image
254 |
255 |
256 |
257 | *
258 |
259 |
260 |
261 |
262 | for post video capture image or reels capture image
263 |
264 |
265 |
266 | *
267 |
268 |
269 |
270 |
271 | for post, story and reels video
272 |
273 |
274 |
275 |
Click on the image or video and the highest resolution version opens in a new tab
276 |
277 | Note : All videos are muted
278 |
279 |
280 |
Result
281 |
282 | );
283 | };
284 |
285 | Gallery.Description = Description;
286 |
287 | export default Gallery;
288 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect, useRef, useState } from 'react';
3 | import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
4 | import toast from 'react-hot-toast';
5 |
6 | import axios from 'axios';
7 |
8 | import {
9 | INSTAGRAM_HIGHLIGHT_ID_REGEX,
10 | INSTAGRAM_HIGHLIGHTSPAGE_REGEX,
11 | INSTAGRAM_POSTPAGE_REGEX,
12 | INSTAGRAM_REELSPAGE_REGEX,
13 | INSTAGRAM_USERNAME_REGEX_FOR_PROFILE,
14 | INSTAGRAM_USERNAME_REGEX_FOR_STORIES,
15 | } from '@/constants/regexes';
16 | import {
17 | INSTAGRAM_GRAPHQL_URL_FOR_HIGHLIGHTS,
18 | INSTAGRAM_GRAPHQL_URL_FOR_POST,
19 | INSTAGRAM_GRAPHQL_URL_FOR_STORIES,
20 | INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS,
21 | } from '@/constants/urls';
22 |
23 | import { useCopyToClipboard } from '@/hooks';
24 |
25 | import IconX from '@/assets/icons/icon-x.svg';
26 |
27 | import fetchProxyImage from '@/services/fetch-proxy-image';
28 |
29 | import Button from '@/components/Buttton/Button';
30 | import Gallery from '@/components/Galllery/Gallery';
31 | import HowToUse from '@/components/HowToUse';
32 | import Input from '@/components/Input';
33 | import Tab from '@/components/Tab';
34 | import TextArea from '@/components/TextArea';
35 |
36 | import styles from './Home.module.scss';
37 |
38 | const Home: React.FC = () => {
39 | const { isCopied, copyToClipboard } = useCopyToClipboard();
40 | const { executeRecaptcha } = useGoogleReCaptcha();
41 |
42 | const [url, setUrl] = useState('');
43 | const [prevUrl, setPrevUrl] = useState(null);
44 |
45 | const [generatedUrl, setGeneratedUrl] = useState('');
46 | const [jsonInput, setJsonInput] = useState('');
47 | const [profilePicture, setProfilePicture] = useState(null);
48 | const [manualProfilePicStep, setManualProfilePicStep] = useState(null);
53 | const [manualJsonInput, setManualJsonInput] = useState('');
54 | const [processedData, setProcessedData] = useState(null);
55 |
56 | // Profile Picture Tab
57 | const [username, setUsername] = useState('');
58 | const [prevUsername, setPrevUsername] = useState('');
59 | const [isLoadingForProfilePicture, setIsLoadingForProfilePicture] = useState(false);
60 |
61 | // For LocalStorage
62 | const [generatedUrls, setGeneratedUrls] = useState([]);
63 |
64 | const handleSaveLocalStorage = ({ url, page, username }: { url: string; page: string; username: string }) => {
65 | const isUrlExists = generatedUrls.some((item: any) => item.url === url);
66 |
67 | if (!isUrlExists) {
68 | const updatedGeneratedUrls = [
69 | ...generatedUrls,
70 | {
71 | url,
72 | page,
73 | username,
74 | },
75 | ];
76 | setGeneratedUrls(updatedGeneratedUrls);
77 | localStorage.setItem('generatedUrls', JSON.stringify(updatedGeneratedUrls));
78 | }
79 | };
80 |
81 | const getInstagramUserId = async (username: string) => {
82 | if (!executeRecaptcha) {
83 | toast.error('Execute recaptcha not available yet');
84 | return;
85 | }
86 |
87 | try {
88 | const token = await executeRecaptcha();
89 | if (!token) {
90 | throw new Error('Unable to retrieve reCAPTCHA token.');
91 | }
92 |
93 | const response = await axios.post(
94 | '/api/get-user-id',
95 | { username, token },
96 | {
97 | headers: {
98 | 'Content-Type': 'application/json',
99 | Accept: 'application/json, text/plain, */*',
100 | },
101 | maxRedirects: 0,
102 | }
103 | );
104 |
105 | const data = response.data;
106 |
107 | return data.userId;
108 | } catch (error: any) {
109 | toast.error(`API error: ${error?.response?.data?.error?.message ?? error.message}`);
110 | }
111 | };
112 |
113 | const handleGenerateUrl = async () => {
114 | setPrevUrl(url);
115 |
116 | if (!url) {
117 | toast.error('Please provide a URL');
118 | return;
119 | }
120 |
121 | const urlObj = new URL(url);
122 |
123 | if (urlObj.protocol) {
124 | urlObj.search = '';
125 |
126 | let finalUrl;
127 |
128 | if (url !== prevUrl) {
129 | if (url.includes('instagram')) {
130 | if (urlObj.href.includes(`${INSTAGRAM_POSTPAGE_REGEX}`)) {
131 | // Extract shortcode from post URL
132 | const match = urlObj.pathname.match(/\/p\/([^/]+)/);
133 | const shortcode = match && match[1];
134 | if (shortcode) {
135 | finalUrl = INSTAGRAM_GRAPHQL_URL_FOR_POST.replace('', shortcode);
136 | finalUrl = encodeURI(finalUrl);
137 | }
138 | } else if (urlObj.href.includes(`${INSTAGRAM_REELSPAGE_REGEX}`)) {
139 | // Extract shortcode from reels URL
140 | const match = urlObj.pathname.match(/\/reel\/([^/]+)/);
141 | const shortcode = match && match[1];
142 | if (shortcode) {
143 | finalUrl = INSTAGRAM_GRAPHQL_URL_FOR_POST.replace('', shortcode);
144 | finalUrl = encodeURI(finalUrl);
145 | }
146 | } else if (INSTAGRAM_HIGHLIGHTSPAGE_REGEX.test(urlObj.href)) {
147 | const match = urlObj.href.match(INSTAGRAM_HIGHLIGHT_ID_REGEX);
148 |
149 | if (match && match[1]) {
150 | finalUrl = INSTAGRAM_GRAPHQL_URL_FOR_HIGHLIGHTS.replace('', match[1]);
151 | finalUrl = encodeURI(finalUrl);
152 | }
153 | } else if (INSTAGRAM_USERNAME_REGEX_FOR_STORIES.test(urlObj.href)) {
154 | const usernameMatch = urlObj.href.match(INSTAGRAM_USERNAME_REGEX_FOR_STORIES);
155 | const username: any = usernameMatch && usernameMatch[1];
156 |
157 | const userId = await getInstagramUserId(username);
158 |
159 | if (userId) {
160 | finalUrl = INSTAGRAM_GRAPHQL_URL_FOR_STORIES.replace('', userId);
161 | finalUrl = encodeURI(finalUrl);
162 | handleSaveLocalStorage({ page: 'Stories', url: finalUrl, username });
163 | }
164 | } else {
165 | const usernameMatch = urlObj.href.match(INSTAGRAM_USERNAME_REGEX_FOR_PROFILE);
166 | const username: any = usernameMatch && usernameMatch[1];
167 |
168 | const userId = await getInstagramUserId(username);
169 |
170 | if (userId) {
171 | finalUrl = INSTAGRAM_GRAPHQL_URL_FOR_STORIES.replace('', userId);
172 | finalUrl = encodeURI(finalUrl);
173 | handleSaveLocalStorage({ page: 'Profile', url: finalUrl, username });
174 | }
175 | }
176 | if (finalUrl) {
177 | setGeneratedUrl(finalUrl);
178 | }
179 | } else {
180 | toast.error('Invalid URL. Must be an Instagram URL.');
181 | }
182 | } else {
183 | toast.error('The URL cannot be the same as the previous one');
184 | }
185 | } else {
186 | toast.error('Invalid URL format');
187 | }
188 | };
189 |
190 | const handleJsonPaste = (event: React.ChangeEvent) => {
191 | setJsonInput(event.target.value);
192 | setProcessedData(event.target.value);
193 | };
194 |
195 | const handleSubmitForProfilePicture = async () => {
196 | if (prevUsername !== username) {
197 | if (!executeRecaptcha) {
198 | toast.error('Execute recaptcha not available yet');
199 | return;
200 | }
201 |
202 | try {
203 | const token = await executeRecaptcha();
204 | if (!token) {
205 | throw new Error('Unable to retrieve reCAPTCHA token.');
206 | }
207 |
208 | setIsLoadingForProfilePicture(true);
209 |
210 | const response = await axios.post(
211 | '/api/get-profile-picture',
212 | { username, token },
213 | {
214 | headers: {
215 | 'Content-Type': 'application/json',
216 | Accept: 'application/json, text/plain, */*',
217 | },
218 | maxRedirects: 0,
219 | }
220 | );
221 |
222 | const data = response.data;
223 |
224 | const image = await fetchProxyImage(data.url);
225 |
226 | setPrevUsername(username);
227 | setProfilePicture({
228 | image,
229 | url: data.url,
230 | });
231 | setManualProfilePicStep(null);
232 | setIsLoadingForProfilePicture(false);
233 | } catch (error: any) {
234 | setIsLoadingForProfilePicture(false);
235 | let errMsg = error.response?.data?.error?.message ?? error.message;
236 | try {
237 | const parsed = JSON.parse(errMsg);
238 | if (parsed && parsed.url && parsed.message) {
239 | setManualProfilePicStep(parsed);
240 | setProfilePicture(null);
241 | return;
242 | }
243 | } catch (e) {}
244 | toast.error(`API error: ${errMsg}`);
245 | }
246 | } else {
247 | toast.error('The username cannot be the same as the previous one');
248 | }
249 | };
250 |
251 | // Kullanıcı manuel JSON yapıştırınca çalışacak fonksiyon
252 | const handleManualJsonSubmit = async (e: any) => {
253 | e.preventDefault();
254 | try {
255 | setManualJsonInput(e.target.value);
256 | const parsed = JSON.parse(e.target.value);
257 | const url = parsed?.data?.user?.hd_profile_pic_url_info?.url;
258 | if (url) {
259 | const image = await fetchProxyImage(url);
260 |
261 | setProfilePicture({
262 | image,
263 | url,
264 | });
265 | setManualProfilePicStep(null);
266 | setPrevUsername(username);
267 | } else {
268 | toast.error('Invalid JSON. Please ensure it contains the correct structure.');
269 | }
270 | } catch (e) {
271 | toast.error('Invalid JSON');
272 | }
273 | };
274 |
275 | const handleDeleteItem = (url: string) => {
276 | const updatedGeneratedUrls = generatedUrls.filter((item: any) => item.url !== url);
277 | setGeneratedUrls(updatedGeneratedUrls);
278 | localStorage.setItem('generatedUrls', JSON.stringify(updatedGeneratedUrls));
279 | };
280 |
281 | useEffect(() => {
282 | if (isCopied) {
283 | toast.success('Copied');
284 | }
285 | }, [isCopied]);
286 |
287 | useEffect(() => {
288 | const storedGeneratedUrls = localStorage.getItem('generatedUrls');
289 | if (storedGeneratedUrls) {
290 | setGeneratedUrls(JSON.parse(storedGeneratedUrls));
291 | }
292 | }, []);
293 |
294 | if (processedData) {
295 | let parsedData;
296 | try {
297 | parsedData = JSON.parse(processedData);
298 | } catch (e) {
299 | toast.error('Invalid JSON');
300 | return null;
301 | }
302 |
303 | // Instagram Post JSON (GraphQL) special handling
304 | if (parsedData?.data?.xdt_shortcode_media) {
305 | // Wrap post data in a structure compatible with Gallery
306 | return ;
307 | }
308 |
309 | // Instagram Profile Posts JSON (GraphQL) special handling
310 | if (parsedData?.data?.xdt_api__v1__feed__user_timeline_graphql_connection?.edges) {
311 | const edges = parsedData.data.xdt_api__v1__feed__user_timeline_graphql_connection.edges;
312 | const items = edges.map((edge: any) => edge.node);
313 | // end_cursor değerini al
314 | const endCursor = parsedData.data.xdt_api__v1__feed__user_timeline_graphql_connection.page_info?.end_cursor;
315 | const usernameForPosts =
316 | parsedData.data.xdt_api__v1__feed__user_timeline_graphql_connection.edges?.[0]?.node.user.username;
317 |
318 | return ;
319 | }
320 |
321 | // Default: pass as is
322 | return ;
323 | }
324 |
325 | return (
326 |
327 |
Instagram Media Downloader
328 |
329 |
330 |
331 | Please enter Instagram story or reels or post url
332 | {
336 | setUrl(e.target.value);
337 | }}
338 | onBlur={async () => {
339 | let input = url.trim();
340 | if (input && !/^https?:\/\//.test(input) && !input.includes('instagram.com')) {
341 | const userId = await getInstagramUserId(input);
342 | if (userId) {
343 | const generated = INSTAGRAM_GRAPHQL_URL_FOR_USER_POSTS.replace('', input);
344 | setGeneratedUrl(generated);
345 | handleSaveLocalStorage({ page: 'UserPosts', url: generated, username: input });
346 | return;
347 | }
348 | }
349 | handleGenerateUrl();
350 | }}
351 | />
352 |
353 | {generatedUrls && (
354 |
355 | Recent Items
356 | {Array.from(new Set(generatedUrls.map((item: any) => item.page))).map((category, categoryIndex) => {
357 | const categoryItems = generatedUrls.filter((item: any) => item.page === category);
358 |
359 | return (
360 |
361 |
{category as string}
362 |
363 | {categoryItems.map((item: any, itemIndex: number) => (
364 |
365 |
copyToClipboard(item.url)}>
366 | {item.username}
367 |
368 |
handleDeleteItem(item.url)}>
369 |
370 |
371 |
372 | ))}
373 |
374 |
375 | );
376 | })}
377 |
378 | )}
379 |
380 |
381 | Copy the generated link and open it in a new tab. Then copy the JSON output from the tab
382 |
383 | {
389 | copyToClipboard(generatedUrl);
390 | }}
391 | />
392 |
393 |
394 | Paste the JSON data into the input below. You will then be redirected to result page 🎉
395 |
396 |
408 |
409 |
410 | Please enter Instagram username below input
411 |
412 | {
416 | setUsername(e.target.value);
417 | }}
418 | />
419 |
420 |
425 | Submit
426 |
427 |
428 |
429 |
430 |
431 | {/* Manuel JSON ile profil fotoğrafı alma adımı */}
432 | {manualProfilePicStep && (
433 |
434 |
{manualProfilePicStep.message}
435 |
copyToClipboard(manualProfilePicStep.url)}
440 | />
441 |
{manualProfilePicStep.instructions}
442 |
456 | )}
457 |
458 | {profilePicture && (
459 | <>
460 | Please tap to enlarge the image
461 |
462 |
463 |
464 | >
465 | )}
466 |
467 |
468 |
469 | );
470 | };
471 |
472 | export default Home;
473 |
--------------------------------------------------------------------------------