diff --git a/package.json b/package.json index f5595b6e..e15b31a5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "5.4", + "version": "5.4.4", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 312b6225..a0c3febb 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -71,7 +71,7 @@ "SettingsAudioFullTikTok": "full audio", "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}.", - "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", "ImagePickerDownloadAudio": "download audio", "ImagePickerExplanationPC": "right click an image to save it.", @@ -118,6 +118,7 @@ "SettingsDubAuto": "auto", "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.", - "ShareURL": "share" + "ShareURL": "share", + "ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index f899a3c3..d971c261 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -71,7 +71,7 @@ "SettingsAudioFullTikTok": "полное аудио", "SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, использованный в видео. без каких-либо изменений от автора поста.", "ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, а если так и не получится, то {ContactLink}.", - "ErrorNoVideosInTweet": "в этом твите нет ни видео, ни гифок. попробуй другой!", + "ErrorNoVideosInTweet": "я не смог найти никакого медиа контента в этом твите. попробуй другой!", "ImagePickerTitle": "выбери картинки для скачивания", "ImagePickerDownloadAudio": "скачать звук", "ImagePickerExplanationPC": "нажми правой кнопкой мыши на картинку, чтобы её сохранить.", @@ -118,6 +118,7 @@ "SettingsDubAuto": "авто", "SettingsVimeoPrefer": "тип загрузок с vimeo", "SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: {appName} совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, {appName} скачает \"dash\".", - "ShareURL": "поделиться" + "ShareURL": "поделиться", + "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!" } } diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js index 3323c1d3..7e010eda 100644 --- a/src/modules/processing/services/twitter.js +++ b/src/modules/processing/services/twitter.js @@ -1,54 +1,73 @@ import { genericUserAgent } from "../../config.js"; +import crypto from "crypto"; 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] } -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) { let _headers = { "user-agent": genericUserAgent, "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", // ^ 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", headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false }); if (!req_act) return { error: 'ErrorCouldntFetch' }; _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) { - let req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false }); - if (!req_status) { + let conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false }); + if (!conversation || !conversation.globalObjects.tweets[obj.id]) { _headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"; // ^ 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", headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false}).catch(() => { return false }); if (!req_act) return { error: 'ErrorCouldntFetch' }; - _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 }); - } - if (!req_status) return { error: 'ErrorCouldntFetch' }; + _headers["x-guest-token"] = req_act["guest_token"] + _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('; '); - let baseStatus; - 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"] + conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false }); } - 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 }) 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"])}) } diff --git a/src/test/tests.json b/src/test/tests.json index b5abc2a8..53301a57 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -97,6 +97,30 @@ } }, { "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", "params": { "aFormat": "mp3",