5.4.4: moved to twitter api v2

This commit is contained in:
wukko 2023-04-27 09:26:19 +06:00
parent 2120cf1101
commit 8f93232e81
5 changed files with 70 additions and 25 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "5.4", "version": "5.4.4",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View file

@ -71,7 +71,7 @@
"SettingsAudioFullTikTok": "full audio", "SettingsAudioFullTikTok": "full audio",
"SettingsAudioFullTikTokDescription": "downloads original sound used in the video without any additional changes by the post's author.", "SettingsAudioFullTikTokDescription": "downloads original sound used in the video without any additional changes by the post's author.",
"ErrorCantGetID": "i couldn't get the full info from the shortened link. make sure it works or try a full one! if issue persists, {ContactLink}.", "ErrorCantGetID": "i couldn't get the full info from the shortened link. make sure it works or try a full one! if issue persists, {ContactLink}.",
"ErrorNoVideosInTweet": "there are no videos or gifs in this tweet, try another one!", "ErrorNoVideosInTweet": "i couldn't find any media content in this tweet. try another one!",
"ImagePickerTitle": "pick images to download", "ImagePickerTitle": "pick images to download",
"ImagePickerDownloadAudio": "download audio", "ImagePickerDownloadAudio": "download audio",
"ImagePickerExplanationPC": "right click an image to save it.", "ImagePickerExplanationPC": "right click an image to save it.",
@ -118,6 +118,7 @@
"SettingsDubAuto": "auto", "SettingsDubAuto": "auto",
"SettingsVimeoPrefer": "vimeo downloads type", "SettingsVimeoPrefer": "vimeo downloads type",
"SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by {appName} into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.", "SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by {appName} into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.",
"ShareURL": "share" "ShareURL": "share",
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!"
} }
} }

View file

@ -71,7 +71,7 @@
"SettingsAudioFullTikTok": "полное аудио", "SettingsAudioFullTikTok": "полное аудио",
"SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, использованный в видео. без каких-либо изменений от автора поста.", "SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, использованный в видео. без каких-либо изменений от автора поста.",
"ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, а если так и не получится, то {ContactLink}.", "ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, а если так и не получится, то {ContactLink}.",
"ErrorNoVideosInTweet": "в этом твите нет ни видео, ни гифок. попробуй другой!", "ErrorNoVideosInTweet": "я не смог найти никакого медиа контента в этом твите. попробуй другой!",
"ImagePickerTitle": "выбери картинки для скачивания", "ImagePickerTitle": "выбери картинки для скачивания",
"ImagePickerDownloadAudio": "скачать звук", "ImagePickerDownloadAudio": "скачать звук",
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на картинку, чтобы её сохранить.", "ImagePickerExplanationPC": "нажми правой кнопкой мыши на картинку, чтобы её сохранить.",
@ -118,6 +118,7 @@
"SettingsDubAuto": "авто", "SettingsDubAuto": "авто",
"SettingsVimeoPrefer": "тип загрузок с vimeo", "SettingsVimeoPrefer": "тип загрузок с vimeo",
"SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: {appName} совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, {appName} скачает \"dash\".", "SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: {appName} совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, {appName} скачает \"dash\".",
"ShareURL": "поделиться" "ShareURL": "поделиться",
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!"
} }
} }

View file

@ -1,54 +1,73 @@
import { genericUserAgent } from "../../config.js"; import { genericUserAgent } from "../../config.js";
import crypto from "crypto";
function bestQuality(arr) { function bestQuality(arr) {
return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0] return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0]
} }
const apiURL = "https://api.twitter.com/1.1" const apiURL = "https://api.twitter.com"
// TO-DO: move from 1.1 api to graphql
export default async function(obj) { export default async function(obj) {
let _headers = { let _headers = {
"user-agent": genericUserAgent, "user-agent": genericUserAgent,
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
// ^ no explicit content, but with multi media support // ^ no explicit content, but with multi media support
"host": "api.twitter.com" "host": "api.twitter.com",
"x-twitter-client-language": "en",
"x-twitter-active-user": "yes",
"Accept-Language": "en"
}; };
let req_act = await fetch(`${apiURL}/guest/activate.json`, { let conversationURL = `${apiURL}/2/timeline/conversation/${obj.id}.json?cards_platform=Web-12&tweet_mode=extended&include_cards=1&include_ext_media_availability=true&include_ext_sensitive_media_warning=true&simple_quoted_tweet=true&trim_user=1`;
let activateURL = `${apiURL}/1.1/guest/activate.json`;
let req_act = await fetch(activateURL, {
method: "POST", method: "POST",
headers: _headers headers: _headers
}).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false }); }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
if (!req_act) return { error: 'ErrorCouldntFetch' }; if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"]; _headers["x-guest-token"] = req_act["guest_token"];
let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`; _headers["cookie"] = [
`guest_id_ads=v1%3A${req_act["guest_token"]}`,
`guest_id_marketing=v1%3A${req_act["guest_token"]}`,
`guest_id=v1%3A${req_act["guest_token"]}`,
`ct0=${crypto.randomUUID().replace(/-/g, '')};`
].join('; ');
if (!obj.spaceId) { if (!obj.spaceId) {
let req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false }); let conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
if (!req_status) { if (!conversation || !conversation.globalObjects.tweets[obj.id]) {
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"; _headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
// ^ explicit content, but no multi media support // ^ explicit content, but no multi media support
delete _headers["x-guest-token"] delete _headers["x-guest-token"];
delete _headers["cookie"];
req_act = await fetch(`${apiURL}/guest/activate.json`, { req_act = await fetch(activateURL, {
method: "POST", method: "POST",
headers: _headers headers: _headers
}).then((r) => { return r.status === 200 ? r.json() : false}).catch(() => { return false }); }).then((r) => { return r.status === 200 ? r.json() : false}).catch(() => { return false });
if (!req_act) return { error: 'ErrorCouldntFetch' }; if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"]; _headers["x-guest-token"] = req_act["guest_token"]
req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false }); _headers['cookie'] = [
} `guest_id_ads=v1%3A${req_act["guest_token"]}`,
if (!req_status) return { error: 'ErrorCouldntFetch' }; `guest_id_marketing=v1%3A${req_act["guest_token"]}`,
`guest_id=v1%3A${req_act["guest_token"]}`,
`ct0=${crypto.randomUUID().replace(/-/g, '')};`
].join('; ');
let baseStatus; conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
baseStatus = req_status["extended_entities"]
} else if (req_status["retweeted_status"] && req_status["retweeted_status"]["extended_entities"] && req_status["retweeted_status"]["extended_entities"]["media"]) {
baseStatus = req_status["retweeted_status"]["extended_entities"]
} }
if (!baseStatus) return { error: 'ErrorNoVideosInTweet' }; if (!conversation || !conversation.globalObjects.tweets[obj.id]) return { error: 'ErrorTweetUnavailable' };
let single, multiple = [], media = baseStatus["media"]; let baseMedia, baseTweet = conversation.globalObjects.tweets[obj.id];
if (baseTweet.retweeted_status_id_str && conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities) {
baseMedia = conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities
} else if (baseTweet.extended_entities && baseTweet.extended_entities.media) {
baseMedia = baseTweet.extended_entities
}
if (!baseMedia) return { error: 'ErrorNoVideosInTweet' };
let single, multiple = [], media = baseMedia["media"];
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true }) media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
if (media.length > 1) { if (media.length > 1) {
for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) }

View file

@ -97,6 +97,30 @@
} }
}, { }, {
"name": "retweeted video", "name": "retweeted video",
"url": "https://twitter.com/winload_exe/status/1639005390854602758",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "retweeted video",
"url": "https://twitter.com/winload_exe/status/1639005390854602758",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "age-restricted video",
"url": "https://twitter.com/FckyeahCharli/status/1650987582749065220",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "retweeted video, isAudioOnly",
"url": "https://twitter.com/winload_exe/status/1633091769482063874", "url": "https://twitter.com/winload_exe/status/1633091769482063874",
"params": { "params": {
"aFormat": "mp3", "aFormat": "mp3",