mirror of
https://github.com/wukko/cobalt.git
synced 2025-03-05 00:18:52 +01:00
feat: threads support
This commit is contained in:
parent
d2e5b6542f
commit
e73ad62b78
7 changed files with 152 additions and 0 deletions
|
@ -26,6 +26,7 @@ this list is not final and keeps expanding over time. if support for a service y
|
||||||
| rutube | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| rutube | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ |
|
| soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ |
|
||||||
| streamable | ✅ | ✅ | ✅ | ➖ | ➖ |
|
| streamable | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||||
|
| threads posts | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| tiktok | ✅ | ✅ | ✅ | ❌ | ❌ |
|
| tiktok | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
| tumblr | ✅ | ✅ | ✅ | ➖ | ➖ |
|
| tumblr | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||||
| twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
@ -49,6 +50,7 @@ this list is not final and keeps expanding over time. if support for a service y
|
||||||
| reddit | supports gifs and videos. |
|
| reddit | supports gifs and videos. |
|
||||||
| rutube | supports yappy & private links. |
|
| rutube | supports yappy & private links. |
|
||||||
| soundcloud | supports private links. |
|
| soundcloud | supports private links. |
|
||||||
|
| threads | supports photos and videos. lets you pick what to save from multi-media posts. |
|
||||||
| tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. |
|
| tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. |
|
||||||
| twitter/x | lets you pick what to save from multi-media posts. may not be 100% reliable due to current management. |
|
| twitter/x | lets you pick what to save from multi-media posts. may not be 100% reliable due to current management. |
|
||||||
| vimeo | audio downloads are only available for dash. |
|
| vimeo | audio downloads are only available for dash. |
|
||||||
|
|
|
@ -25,6 +25,7 @@ import twitch from "./services/twitch.js";
|
||||||
import rutube from "./services/rutube.js";
|
import rutube from "./services/rutube.js";
|
||||||
import dailymotion from "./services/dailymotion.js";
|
import dailymotion from "./services/dailymotion.js";
|
||||||
import loom from "./services/loom.js";
|
import loom from "./services/loom.js";
|
||||||
|
import threads from "./services/threads.js";
|
||||||
|
|
||||||
let freebind;
|
let freebind;
|
||||||
|
|
||||||
|
@ -193,6 +194,13 @@ export default async function(host, patternMatch, lang, obj) {
|
||||||
id: patternMatch.id
|
id: patternMatch.id
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "threads":
|
||||||
|
r = await threads({
|
||||||
|
...patternMatch,
|
||||||
|
quality: obj.vQuality,
|
||||||
|
dispatcher
|
||||||
|
})
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return createResponse("error", {
|
return createResponse("error", {
|
||||||
t: loc(lang, 'ErrorUnsupported')
|
t: loc(lang, 'ErrorUnsupported')
|
||||||
|
|
|
@ -67,6 +67,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
||||||
switch (host) {
|
switch (host) {
|
||||||
case "instagram":
|
case "instagram":
|
||||||
case "twitter":
|
case "twitter":
|
||||||
|
case "threads":
|
||||||
params = { picker: r.picker };
|
params = { picker: r.picker };
|
||||||
break;
|
break;
|
||||||
case "tiktok":
|
case "tiktok":
|
||||||
|
@ -130,6 +131,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
||||||
case "pinterest":
|
case "pinterest":
|
||||||
case "streamable":
|
case "streamable":
|
||||||
case "loom":
|
case "loom":
|
||||||
|
case "threads":
|
||||||
responseType = "redirect";
|
responseType = "redirect";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
106
src/modules/processing/services/threads.js
Normal file
106
src/modules/processing/services/threads.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { createStream } from "../../stream/manage.js";
|
||||||
|
import { getCookie, updateCookie } from "../cookie/manager.js";
|
||||||
|
|
||||||
|
const commonHeaders = {
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.7",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Dnt": "1",
|
||||||
|
"Priority": "u=0, i",
|
||||||
|
"Sec-Ch-Ua": '"Not/A)Brand";v="8", "Chromium";v="126", "Brave";v="126"',
|
||||||
|
"Sec-Ch-Ua-Mobile": "?0",
|
||||||
|
"Sec-Ch-Ua-Model": '""',
|
||||||
|
"Sec-Ch-Ua-Platform": '"Windows"',
|
||||||
|
"Sec-Ch-Ua-Platform-Version": '"15.0.0"',
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "same-origin",
|
||||||
|
"Sec-Gpc": "1",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DATA_REGEX = /<script type="application\/json" data-content-len="\d+" data-sjs>({"require":\[\["ScheduledServerJS","handle",null,\[{"__bbox":{"require":\[\["RelayPrefetchedStreamCache(?:(?:@|\\u0040)[0-9a-f]{32})?","next",\[],\["adp_BarcelonaPostPageQueryRelayPreloader_[0-9a-f]{23}",[^\n]+})<\/script>\n/;
|
||||||
|
|
||||||
|
export default async function({ user, id, quality, dispatcher }) {
|
||||||
|
const cookie = getCookie('threads');
|
||||||
|
const response = await fetch(`https://www.threads.net/${user}/post/${id}`, {
|
||||||
|
headers: {
|
||||||
|
...commonHeaders,
|
||||||
|
cookie
|
||||||
|
},
|
||||||
|
dispatcher
|
||||||
|
});
|
||||||
|
if (cookie) updateCookie(cookie, data.headers);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return { error: 'ErrorCouldntFetch' };
|
||||||
|
}
|
||||||
|
const html = await response.text();
|
||||||
|
const dataString = html.match(DATA_REGEX)?.[1];
|
||||||
|
if (!dataString) {
|
||||||
|
return { error: 'ErrorCouldntFetch' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(dataString);
|
||||||
|
const post = data?.require?.[0]?.[3]?.[0]?.__bbox?.require?.[0]?.[3]?.[1]?.__bbox?.result?.data?.data?.edges[0]?.node?.thread_items[0]?.post;
|
||||||
|
if (!post) {
|
||||||
|
return { error: 'ErrorCouldntFetch' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video
|
||||||
|
if (post.media_type === 2) {
|
||||||
|
if (!post.video_versions) {
|
||||||
|
return { error: 'ErrorEmptyDownload' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// types: 640p = 101, 480p = 102, 480p-low = 103
|
||||||
|
const selectedQualityType = quality === 'max' ? 101 : parseInt(quality) > 480 ? 102 : 101;
|
||||||
|
const video = post.video_versions.find((v) => v.type === selectedQualityType) || post.video_versions.sort((a, b) => a.type - b.type)[0];
|
||||||
|
if (!video) {
|
||||||
|
return { error: 'ErrorEmptyDownload' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
urls: video.url,
|
||||||
|
filename: `threads_${user}_${id}.mp4`,
|
||||||
|
audioFilename: `threads_${user}_${id}_audio`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photo
|
||||||
|
if (post.media_type === 1) {
|
||||||
|
if (!post.image_versions2?.candidates) {
|
||||||
|
return { error: 'ErrorEmptyDownload' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
urls: post.image_versions2.candidates[0].url,
|
||||||
|
isPhoto: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mixed
|
||||||
|
if (post.media_type === 8) {
|
||||||
|
if (!post.carousel_media) {
|
||||||
|
return { error: 'ErrorEmptyDownload' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
picker: post.carousel_media.map((media) => ({
|
||||||
|
type: media.video_versions ? 'video' : 'photo',
|
||||||
|
url: media.video_versions ? media.video_versions[0].url : media.image_versions2.candidates[0].url,
|
||||||
|
/* thumbnails have `Cross-Origin-Resource-Policy`
|
||||||
|
** set to `same-origin`, so we need to proxy them */
|
||||||
|
thumb: createStream({
|
||||||
|
service: "instagram",
|
||||||
|
type: "default",
|
||||||
|
u: media.image_versions2.candidates[0].url,
|
||||||
|
filename: "image.jpg"
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: 'ErrorUnsupported' };
|
||||||
|
}
|
|
@ -117,6 +117,12 @@
|
||||||
"alias": "loom videos",
|
"alias": "loom videos",
|
||||||
"patterns": ["share/:id"],
|
"patterns": ["share/:id"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
},
|
||||||
|
"threads": {
|
||||||
|
"alias": "threads posts",
|
||||||
|
"tld": "net",
|
||||||
|
"patterns": [":user/post/:id"],
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,9 @@ export const testers = {
|
||||||
"streamable": (patternMatch) =>
|
"streamable": (patternMatch) =>
|
||||||
patternMatch.id?.length === 6,
|
patternMatch.id?.length === 6,
|
||||||
|
|
||||||
|
"threads": (patternMatch) =>
|
||||||
|
patternMatch.user?.length <= 33 && patternMatch.id?.length <= 32,
|
||||||
|
|
||||||
"tiktok": (patternMatch) =>
|
"tiktok": (patternMatch) =>
|
||||||
patternMatch.postId?.length <= 21 || patternMatch.id?.length <= 13,
|
patternMatch.postId?.length <= 21 || patternMatch.id?.length <= 13,
|
||||||
|
|
||||||
|
|
|
@ -1160,5 +1160,30 @@
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"status": "stream"
|
"status": "stream"
|
||||||
}
|
}
|
||||||
|
}],
|
||||||
|
"threads": [{
|
||||||
|
"name": "video",
|
||||||
|
"url": "https://www.threads.net/@zuck/post/CzecNnZPaxr",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "redirect"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": "photo",
|
||||||
|
"url": "https://www.threads.net/@soren.iverson/post/C8PdJ59pMLr",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "redirect"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": "mixed media",
|
||||||
|
"url": "https://www.threads.net/@snazzahguy/post/C8Q7UZDseWz",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "picker"
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue