merge: updates from main

This commit is contained in:
wukko 2025-03-13 14:56:49 +06:00
commit 2197d9411e
No known key found for this signature in database
GPG Key ID: 3E30B3F26C7B4AA2
6 changed files with 119 additions and 31 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "@imput/cobalt-api", "name": "@imput/cobalt-api",
"description": "save what you love", "description": "save what you love",
"version": "10.7.7", "version": "10.7.9",
"author": "imput", "author": "imput",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View File

@ -23,7 +23,7 @@ export default async function(o) {
const videoLink = [...html.matchAll(videoRegex)] const videoLink = [...html.matchAll(videoRegex)]
.map(([, link]) => link) .map(([, link]) => link)
.find(a => a.endsWith('.mp4') && a.includes('720p')); .find(a => a.endsWith('.mp4'));
if (videoLink) return { if (videoLink) return {
urls: videoLink, urls: videoLink,

View File

@ -24,6 +24,11 @@ const badContainerEnd = new Date(1702605600000);
function needsFixing(media) { function needsFixing(media) {
const representativeId = media.source_status_id_str ?? media.id_str; const representativeId = media.source_status_id_str ?? media.id_str;
// syndication api doesn't have media ids in its response,
// so we just assume it's all good
if (!representativeId) return false;
const mediaTimestamp = new Date( const mediaTimestamp = new Date(
Number((BigInt(representativeId) >> 22n) + TWITTER_EPOCH) Number((BigInt(representativeId) >> 22n) + TWITTER_EPOCH)
); );
@ -53,6 +58,25 @@ const getGuestToken = async (dispatcher, forceReload = false) => {
} }
} }
const requestSyndication = async(dispatcher, tweetId) => {
// thank you
// https://github.com/yt-dlp/yt-dlp/blob/05c8023a27dd37c49163c0498bf98e3e3c1cb4b9/yt_dlp/extractor/twitter.py#L1334
const token = (id) => ((Number(id) / 1e15) * Math.PI).toString(36).replace(/(0+|\.)/g, '');
const syndicationUrl = new URL("https://cdn.syndication.twimg.com/tweet-result");
syndicationUrl.searchParams.set("id", tweetId);
syndicationUrl.searchParams.set("token", token(tweetId));
const result = await fetch(syndicationUrl, {
headers: {
"user-agent": genericUserAgent
},
dispatcher
});
return result;
}
const requestTweet = async(dispatcher, tweetId, token, cookie) => { const requestTweet = async(dispatcher, tweetId, token, cookie) => {
const graphqlTweetURL = new URL(graphqlURL); const graphqlTweetURL = new URL(graphqlURL);
@ -87,36 +111,24 @@ const requestTweet = async(dispatcher, tweetId, token, cookie) => {
let result = await fetch(graphqlTweetURL, { headers, dispatcher }); let result = await fetch(graphqlTweetURL, { headers, dispatcher });
updateCookie(cookie, result.headers); updateCookie(cookie, result.headers);
// we might have been missing the `ct0` cookie, retry // we might have been missing the ct0 cookie, retry
if (result.status === 403 && result.headers.get('set-cookie')) { if (result.status === 403 && result.headers.get('set-cookie')) {
const cookieValues = cookie?.values();
if (cookieValues?.ct0) {
result = await fetch(graphqlTweetURL, { result = await fetch(graphqlTweetURL, {
headers: { headers: {
...headers, ...headers,
'x-csrf-token': cookie.values().ct0 'x-csrf-token': cookieValues.ct0
}, },
dispatcher dispatcher
}); });
} }
}
return result return result
} }
export default async function({ id, index, toGif, dispatcher, alwaysProxy }) { const extractGraphqlMedia = async (tweet, dispatcher, id, guestToken, cookie) => {
const cookie = await getCookie('twitter');
let guestToken = await getGuestToken(dispatcher);
if (!guestToken) return { error: "fetch.fail" };
let tweet = await requestTweet(dispatcher, id, guestToken);
// get new token & retry if old one expired
if ([403, 429].includes(tweet.status)) {
guestToken = await getGuestToken(dispatcher, true);
tweet = await requestTweet(dispatcher, id, guestToken)
}
tweet = await tweet.json();
let tweetTypename = tweet?.data?.tweetResult?.result?.__typename; let tweetTypename = tweet?.data?.tweetResult?.result?.__typename;
if (!tweetTypename) { if (!tweetTypename) {
@ -127,13 +139,13 @@ export default async function({ id, index, toGif, dispatcher, alwaysProxy }) {
const reason = tweet?.data?.tweetResult?.result?.reason; const reason = tweet?.data?.tweetResult?.result?.reason;
switch(reason) { switch(reason) {
case "Protected": case "Protected":
return { error: "content.post.private" } return { error: "content.post.private" };
case "NsfwLoggedOut": case "NsfwLoggedOut":
if (cookie) { if (cookie) {
tweet = await requestTweet(dispatcher, id, guestToken, cookie); tweet = await requestTweet(dispatcher, id, guestToken, cookie);
tweet = await tweet.json(); tweet = await tweet.json();
tweetTypename = tweet?.data?.tweetResult?.result?.__typename; tweetTypename = tweet?.data?.tweetResult?.result?.__typename;
} else return { error: "content.post.age" } } else return { error: "content.post.age" };
} }
} }
@ -150,7 +162,69 @@ export default async function({ id, index, toGif, dispatcher, alwaysProxy }) {
repostedTweet = baseTweet?.retweeted_status_result?.result.tweet.legacy.extended_entities; repostedTweet = baseTweet?.retweeted_status_result?.result.tweet.legacy.extended_entities;
} }
let media = (repostedTweet?.media || baseTweet?.extended_entities?.media); return (repostedTweet?.media || baseTweet?.extended_entities?.media);
}
const testResponse = (result) => {
const contentLength = result.headers.get("content-length");
if (!contentLength || contentLength === '0') {
return false;
}
if (!result.headers.get("content-type").startsWith("application/json")) {
return false;
}
return true;
}
export default async function({ id, index, toGif, dispatcher, alwaysProxy }) {
const cookie = await getCookie('twitter');
let syndication = false;
let guestToken = await getGuestToken(dispatcher);
if (!guestToken) return { error: "fetch.fail" };
// for now we assume that graphql api will come back after some time,
// so we try it first
let tweet = await requestTweet(dispatcher, id, guestToken);
// get new token & retry if old one expired
if ([403, 429].includes(tweet.status)) {
guestToken = await getGuestToken(dispatcher, true);
if (cookie) {
tweet = await requestTweet(dispatcher, id, guestToken, cookie);
} else {
tweet = await requestTweet(dispatcher, id, guestToken);
}
}
const testGraphql = testResponse(tweet);
// if graphql requests fail, then resort to tweet embed api
if (!testGraphql) {
syndication = true;
tweet = await requestSyndication(dispatcher, id);
const testSyndication = testResponse(tweet);
// if even syndication request failed, then cry out loud
if (!testSyndication) {
return { error: "fetch.fail" };
}
}
tweet = await tweet.json();
let media =
syndication
? tweet.mediaDetails
: await extractGraphqlMedia(tweet, dispatcher, id, guestToken, cookie);
if (!media) return { error: "fetch.empty" };
// check if there's a video at given index (/video/<index>) // check if there's a video at given index (/video/<index>)
if (index >= 0 && index < media?.length) { if (index >= 0 && index < media?.length) {
@ -163,7 +237,7 @@ export default async function({ id, index, toGif, dispatcher, alwaysProxy }) {
service: "twitter", service: "twitter",
type: "proxy", type: "proxy",
url, filename, url, filename,
}) });
switch (media?.length) { switch (media?.length) {
case undefined: case undefined:

View File

@ -428,6 +428,10 @@ export default async function (o) {
} }
} }
if (video?.drm_families || audio?.drm_families) {
return { error: "youtube.drm" };
}
const fileMetadata = { const fileMetadata = {
title: basicInfo.title.trim(), title: basicInfo.title.trim(),
artist: basicInfo.author.replace("- Topic", "").trim() artist: basicInfo.author.replace("- Topic", "").trim()

View File

@ -169,6 +169,15 @@
"status": "tunnel" "status": "tunnel"
} }
}, },
{
"name": "gif",
"url": "https://x.com/thelastromances/status/1897839691212202479",
"params": {},
"expected": {
"code": 200,
"status": "tunnel"
}
},
{ {
"name": "inexistent post", "name": "inexistent post",
"url": "https://twitter.com/test/status/9487653", "url": "https://twitter.com/test/status/9487653",

View File

@ -67,5 +67,6 @@
"api.youtube.token_expired": "couldn't get this video because the youtube token expired and i couldn't refresh it. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", "api.youtube.token_expired": "couldn't get this video because the youtube token expired and i couldn't refresh it. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!",
"api.youtube.no_hls_streams": "couldn't find any matching HLS streams for this video. try downloading it without HLS!", "api.youtube.no_hls_streams": "couldn't find any matching HLS streams for this video. try downloading it without HLS!",
"api.youtube.api_error": "youtube updated something about its api and i couldn't get any info about this video. try again in a few seconds, but if this issue sticks, please report it!", "api.youtube.api_error": "youtube updated something about its api and i couldn't get any info about this video. try again in a few seconds, but if this issue sticks, please report it!",
"api.youtube.temporary_disabled": "youtube downloading is temporarily disabled due to restrictions from youtube's side. we're already looking for ways to go around them.\n\nwe apologize for the inconvenience and are doing our best to restore this functionality. check cobalt's socials or github for timely updates!" "api.youtube.temporary_disabled": "youtube downloading is temporarily disabled due to restrictions from youtube's side. we're already looking for ways to go around them.\n\nwe apologize for the inconvenience and are doing our best to restore this functionality. check cobalt's socials or github for timely updates!",
"api.youtube.drm": "this youtube video is protected by widevine DRM, so i can't download it. try a different link!"
} }