nexo-aio-downloader
Version:
Download video/photo from multiple sites
213 lines (185 loc) • 8.46 kB
JavaScript
const axios = require('axios');
const querystring = require('querystring');
const { successResponse, errorResponse } = require('./utils');
const defaultFeatures = {
creator_subscriptions_tweet_preview_api_enabled: true,
premium_content_api_read_enabled: false,
communities_web_enable_tweet_community_results_fetch: true,
c9s_tweet_anatomy_moderator_badge_enabled: true,
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
responsive_web_grok_analyze_post_followups_enabled: false,
responsive_web_jetfuel_frame: false,
responsive_web_grok_share_attachment_enabled: true,
articles_preview_enabled: true,
responsive_web_edit_tweet_api_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
view_counts_everywhere_api_enabled: true,
longform_notetweets_consumption_enabled: true,
responsive_web_twitter_article_tweet_consumption_enabled: true,
tweet_awards_web_tipping_enabled: false,
responsive_web_grok_show_grok_translated_post: false,
responsive_web_grok_analysis_button_from_backend: false,
creator_subscriptions_quote_tweet_preview_enabled: false,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
longform_notetweets_rich_text_read_enabled: true,
longform_notetweets_inline_media_enabled: true,
profile_label_improvements_pcf_label_in_post_enabled: true,
rweb_tipjar_consumption_enabled: true,
responsive_web_graphql_exclude_directive_enabled: true,
verified_phone_label_enabled: false,
responsive_web_grok_image_annotation_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_enhance_cards_enabled: false,
};
const defaultVariables = {
withCommunity: false,
includePromotedContent: false,
withVoice: false,
};
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0';
const DEFAULT_HEADERS = {
'User-Agent': USER_AGENT,
Accept: '*/*',
'Accept-Language': 'en-US, en, *;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
TE: 'trailers',
};
const tokenCache = new Map();
async function getTokens(tweetUrl) {
if (tokenCache.has(tweetUrl)) return tokenCache.get(tweetUrl);
try {
const { data } = await axios.get(tweetUrl, { headers: DEFAULT_HEADERS });
const mainJsUrlMatch = data.match(/https:\/\/abs\.twimg\.com\/responsive-web\/(?:client-web-legacy|client-web)\/main\.[^.]+\.js/);
if (!mainJsUrlMatch) throw new Error('Failed to find main JS URL.');
const mainJsUrl = mainJsUrlMatch[0];
const bearerToken = await extractBearerToken(mainJsUrl);
const guestToken = await getGuestToken(data);
const tokens = { bearer: bearerToken, guest: guestToken };
tokenCache.set(tweetUrl, tokens);
return tokens;
} catch (error) {
console.error('Error getting tokens:', error);
throw error;
}
}
async function extractBearerToken(mainJsUrl) {
const { data } = await axios.get(mainJsUrl);
const match = data.match(/Bearer\s+([A-Za-z0-9_%-]{20,})/);
if (!match) throw new Error('Failed to find bearer token.');
return match[1];
}
async function getGuestToken(data) {
const match = data.match(/gt=(\d+)/);
if (!match) throw new Error('Failed to find guest token.');
return match[1];
}
function extractTweetId(tweetUrl) {
const match = tweetUrl.match(/(?<=status\/)\d+/);
if (!match) throw new Error('Could not parse tweet id from URL. Make sure you are using the correct URL.');
return match[0];
}
function updateFeaturesAndVariables(errorData, features, variables) {
if (!errorData?.errors || !Array.isArray(errorData.errors)) return;
errorData.errors.forEach(error => {
if (!error?.message) return;
const neededVars = error.message.match(/Variable '([^']+)'/);
if (neededVars) neededVars.slice(1).forEach(v => (variables[v] = true));
const neededFeatures = error.message.match(/The following features cannot be null: ([^"]+)/);
if (neededFeatures) {
neededFeatures[1].split(',').forEach(feature => (features[feature.trim()] = true));
}
});
}
function getDetailsUrl(tID, features, variables) {
const variablesEncoded = querystring.escape(JSON.stringify({ ...variables, tweetId: tID }));
const featuresEncoded = querystring.escape(JSON.stringify(features));
return `https://api.x.com/graphql/Vg2Akr5FzUmF0sTplA5k6g/TweetResultByRestId?variables=${variablesEncoded}&features=${featuresEncoded}`;
}
function extractMp4s(data) {
if (!data?.core || !data?.legacy || !data?.views) {
return errorResponse('Invalid data format from Twitter API');
}
const { core, legacy, views } = data;
const author = core.user_results.result.legacy.screen_name;
let description = legacy.full_text.replace(/https:\/\/t\.co\/[a-zA-Z0-9_-]+\s*$/, '').trim();
const listUrl =
legacy.entities?.media
?.map(media => {
if (media.type === 'video' || media.type === 'animated_gif') {
const variants = media.video_info?.variants;
if (variants?.length > 0) {
const mp4Variants = variants.filter(v => v.bitrate !== undefined);
if (mp4Variants.length > 0) {
const best = mp4Variants.reduce((p, c) => (c.bitrate > p.bitrate ? c : p));
return {
type: media.type === 'video' ? 'video' : 'gif',
thumb: media.media_url_https,
url: best.url,
};
}
}
return null;
} else if (media.type === 'photo') {
return { type: 'image', url: `${media.media_url_https}?format=png&name=large` };
}
return null;
})
.filter(Boolean) || [];
return successResponse({
author,
like: legacy.favorite_count,
view: views.count,
retweet: legacy.retweet_count,
description,
sensitiveContent: legacy.possibly_sensitive,
result: listUrl,
});
}
async function twitterDownloader(tweetUrl) {
let features = { ...defaultFeatures };
let variables = { ...defaultVariables };
try {
const token = await getTokens(tweetUrl);
if (!token.bearer || !token.guest) throw new Error('Failed to get bearer token.');
const { guest, bearer } = token;
const tweetId = extractTweetId(tweetUrl);
let url = getDetailsUrl(tweetId, features, variables);
let details;
for (let retries = 0; retries < 3; retries++) {
try {
const { data, status } = await axios.get(url, {
headers: {
Authorization: `Bearer ${bearer}`,
'X-Guest-Token': guest,
},
});
if (status === 200 && data?.data?.tweetResult?.result) {
details = data.data.tweetResult.result;
break;
}
console.warn(`Request failed with status ${status}.`);
throw new Error(`Request failed with status ${status}`);
} catch (error) {
if (error.response?.status === 400 && error.response.data) {
updateFeaturesAndVariables(error.response.data, features, variables);
url = getDetailsUrl(tweetId, features, variables);
} else {
console.error('Error fetching details:', error);
throw error;
}
}
}
if (!details) {
return errorResponse('Failed to retrieve tweet details after multiple retries.');
}
return extractMp4s(details);
} catch (error) {
console.error('General error:', error);
tokenCache.delete(tweetUrl);
return errorResponse(error.message || 'An unexpected error occurred');
}
}
module.exports = twitterDownloader;