Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 130 additions & 50 deletions packages/platforms/src/platforms/instagram.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,176 @@
import { Platform } from "../types";
import type { IGMedia } from "./instagram.d";
import * as cheerio from "cheerio";

import { NormalizedPost, Platform } from "../types";

const MATCH_RE =
/^(?:https?:\/\/)?(?:[\w-]+\.)*instagram\.com\/(?:[A-Za-z0-9_.]+\/)?(p|share|reels|reel)\/(?<ig_shortcode>[A-Za-z0-9-_]+)/;
/^(?:https?:\/\/)?(?:[\w-]+\.)*instagram\.com\/(?:[A-Za-z0-9_.]+\/)?(?<ig_type>p|share|reels|reel)\/(?<ig_shortcode>[A-Za-z0-9-_]+)/;

const PRELOADER_PREFIX = "adp_PolarisLoggedOutDesktopWWWPostRootContentQueryRelayPreloader_";

interface InstagramImageCandidate {
url: string;
}

interface InstagramMedia {
__typename?: string;
code: string;
taken_at: number;
caption?: { text?: string } | null;
user: {
username: string;
full_name?: string;
profile_pic_url: string;
};
like_count?: number;
comment_count?: number;
product_type?: string;
play_count?: number;
video_play_count?: number;
view_count?: number;
accessibility_caption?: string;
image_versions2?: {
candidates?: InstagramImageCandidate[];
};
video_versions?: Array<{ url: string }>;
carousel_media?: InstagramMedia[];
}

function normalizeType(type: string) {
if (type === "reels") return "reel";
return type;
}

function firstImage(raw: InstagramMedia) {
return raw.image_versions2?.candidates?.[0]?.url;
}

function mediaURL(raw: InstagramMedia) {
const video = raw.video_versions?.[0]?.url;
if (video) return { url: video, type: "video", description: raw.accessibility_caption };

const image = firstImage(raw);
if (image) return { url: image, type: "photo", description: raw.accessibility_caption };

return null;
}

function parseMedia(raw: InstagramMedia): NormalizedPost["media"] {
if (raw.carousel_media) {
return raw.carousel_media.map(mediaURL).filter((media) => media !== null);
}

const media = mediaURL(raw);
if (!media) return [];

return [media];
}

function parseRelayMedia(script: string) {
const data = JSON.parse(script);
const requireItems = data?.require?.[0]?.[3]?.[0]?.__bbox?.require;
if (!Array.isArray(requireItems)) return null;

const relayRequire = requireItems.find(
(item) =>
item[0] === "RelayPrefetchedStreamCache" && item[3]?.[0]?.startsWith(PRELOADER_PREFIX),
);

export const Instagram: Platform<"Instagram", IGMedia, {}> = {
return relayRequire?.[3]?.[1]?.__bbox?.result?.data?.xig_polaris_media
?.if_not_gated_logged_out as InstagramMedia | undefined;
}

export const Instagram: Platform<"Instagram", InstagramMedia, {}> = {
type: "Instagram",
async match(url) {
async match(url, env) {
if (url.includes("share")) {
const req = await fetch(url.endsWith("/") ? url : `${url}/`, {
redirect: "follow",
headers: {
"User-Agent": "curl/8.7.1",
"User-Agent": env?.EMBED_USER_AGENT ?? "curl/8.7.1",
},
});
url = req.url;
}

const groups = url.match(MATCH_RE)?.groups;
return groups?.ig_shortcode ?? null;
if (!groups) return null;

const type = normalizeType(groups.ig_type);
return `${type}/${groups.ig_shortcode}`;
},
async fetch(id, env) {
const graphql = new URL(`https://www.instagram.com/api/graphql`);
graphql.searchParams.set("variables", JSON.stringify({ shortcode: id }));
graphql.searchParams.set("doc_id", "10015901848480474");
graphql.searchParams.set("lsd", "AVqbxe3J_YA");

const resp = await fetch(graphql.toString(), {
method: "POST",
const [type, shortcode] = id.includes("/") ? id.split("/") : ["p", id];
const resp = await fetch(`https://www.instagram.com/${normalizeType(type)}/${shortcode}/`, {
method: "GET",
headers: {
"User-Agent": env?.EMBED_USER_AGENT ?? "",
"Content-Type": "application/x-www-form-urlencoded",
"X-IG-App-ID": "936619743392459",
"X-FB-LSD": "AVqbxe3J_YA",
"X-ASBD-ID": "129477",
"Sec-Fetch-Site": "same-origin",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
},
});

if (!resp.ok) {
throw { code: resp.status, message: resp.statusText };
}

const { data } = (await resp.json()) as { data?: { xdt_shortcode_media?: IGMedia } };
const media = data?.xdt_shortcode_media;
const html = await resp.text();
const $ = cheerio.load(html);
const script = $('script[type="application/json"][data-sjs]')
.toArray()
.map((el) => $(el).text())
.find(
(text) => text.includes("RelayPrefetchedStreamCache") && text.includes(PRELOADER_PREFIX),
);

if (!script) {
throw {
code: 500,
message: "Instagram page structure changed: missing data script",
};
}

let media: InstagramMedia | undefined | null;
try {
media = parseRelayMedia(script);
} catch {
throw { code: 500, message: "Failed to parse Instagram data" };
}

if (!media) {
throw {
code: 500,
message: "Instagram API returned unexpected structure",
message: "Instagram page structure changed: missing media data",
};
}

return media;
},
async transform(raw) {
const caption = raw.edge_media_to_caption.edges[0]?.node.text;
const authorName = raw.owner.full_name || raw.owner.username;

let media: Array<{ url: string; type: string }>;
let views: number | undefined;

if (raw.__typename === "XDTGraphVideo") {
media = [{ url: raw.video_url, type: "video" }];
views = raw.video_play_count;
} else if (raw.__typename === "XDTGraphSidecar") {
media = raw.edge_sidecar_to_children.edges.map(({ node }) =>
node.__typename === "XDTGraphVideo"
? { url: node.video_url, type: "video" }
: { url: node.display_url, type: "photo" },
);
} else {
media = [{ url: raw.display_url, type: "photo" }];
}
const authorName = raw.user.full_name || raw.user.username;
const path = raw.product_type === "clips" ? "reel" : "p";

return {
platform: this.type,
author: {
name: authorName,
handle: raw.owner.username,
url: `https://www.instagram.com/${raw.owner.username}/`,
avatar: raw.owner.profile_pic_url,
handle: raw.user.username,
url: `https://www.instagram.com/${raw.user.username}/`,
avatar: raw.user.profile_pic_url,
},
url: `https://www.instagram.com/p/${raw.shortcode}/`,
text: caption,
timestamp: raw.taken_at_timestamp,
url: `https://www.instagram.com/${path}/${raw.code}/`,
text: raw.caption?.text,
timestamp: raw.taken_at,
stats: {
likes: raw.edge_media_preview_like.count,
comments: raw.edge_media_to_comment.count,
views,
likes: raw.like_count,
comments: raw.comment_count,
views: raw.play_count ?? raw.video_play_count ?? raw.view_count,
},
media,
media: parseMedia(raw),
};
},
} as const;
96 changes: 96 additions & 0 deletions packages/platforms/src/platforms/instagram_old.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Platform } from "../types";
import type { IGMedia } from "./instagram.d";

const MATCH_RE =
/^(?:https?:\/\/)?(?:[\w-]+\.)*instagram\.com\/(?:[A-Za-z0-9_.]+\/)?(p|share|reels|reel)\/(?<ig_shortcode>[A-Za-z0-9-_]+)/;

export const Instagram: Platform<"Instagram", IGMedia, {}> = {
type: "Instagram",
async match(url) {
if (url.includes("share")) {
const req = await fetch(url.endsWith("/") ? url : `${url}/`, {
redirect: "follow",
headers: {
"User-Agent": "curl/8.7.1",
},
});
url = req.url;
}

const groups = url.match(MATCH_RE)?.groups;
return groups?.ig_shortcode ?? null;
},
async fetch(id, env) {
const graphql = new URL(`https://www.instagram.com/api/graphql`);
graphql.searchParams.set("variables", JSON.stringify({ shortcode: id }));
graphql.searchParams.set("doc_id", "10015901848480474");
graphql.searchParams.set("lsd", "AVqbxe3J_YA");

const resp = await fetch(graphql.toString(), {
method: "POST",
headers: {
"User-Agent": env?.EMBED_USER_AGENT ?? "",
"Content-Type": "application/x-www-form-urlencoded",
"X-IG-App-ID": "936619743392459",
"X-FB-LSD": "AVqbxe3J_YA",
"X-ASBD-ID": "129477",
"Sec-Fetch-Site": "same-origin",
},
});

if (!resp.ok) {
throw { code: resp.status, message: resp.statusText };
}

const { data } = (await resp.json()) as { data?: { xdt_shortcode_media?: IGMedia } };
const media = data?.xdt_shortcode_media;

if (!media) {
throw {
code: 500,
message: "Instagram API returned unexpected structure",
};
}

return media;
},
async transform(raw) {
const caption = raw.edge_media_to_caption.edges[0]?.node.text;
const authorName = raw.owner.full_name || raw.owner.username;

let media: Array<{ url: string; type: string }>;
let views: number | undefined;

if (raw.__typename === "XDTGraphVideo") {
media = [{ url: raw.video_url, type: "video" }];
views = raw.video_play_count;
} else if (raw.__typename === "XDTGraphSidecar") {
media = raw.edge_sidecar_to_children.edges.map(({ node }) =>
node.__typename === "XDTGraphVideo"
? { url: node.video_url, type: "video" }
: { url: node.display_url, type: "photo" },
);
} else {
media = [{ url: raw.display_url, type: "photo" }];
}

return {
platform: this.type,
author: {
name: authorName,
handle: raw.owner.username,
url: `https://www.instagram.com/${raw.owner.username}/`,
avatar: raw.owner.profile_pic_url,
},
url: `https://www.instagram.com/p/${raw.shortcode}/`,
text: caption,
timestamp: raw.taken_at_timestamp,
stats: {
likes: raw.edge_media_preview_like.count,
comments: raw.edge_media_to_comment.count,
views,
},
media,
};
},
} as const;
Loading