-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
Copy paththreads.js
142 lines (123 loc) · 4.96 KB
/
threads.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { createStream } from "../../stream/manage.js";
import { getCookie, updateCookie } from "../cookie/manager.js";
import { genericUserAgent } from "../../config.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": genericUserAgent,
};
const DATA_REGEX = /<script type="application\/json" {2}data-content-len="\d+" data-sjs>({"require":\[\["ScheduledServerJS","handle",null,\[{"__bbox":{"require":\[\["RelayPrefetchedStreamCache(?:(?:@|\\u0040)[0-9a-f]{32})?","next",\[],\["adp_BarcelonaPostPage(?:Direct)?QueryRelayPreloader_[0-9a-f]{23}",[^\n]+})<\/script>\n/;
export default async function({ user, id, quality, alwaysProxy, dispatcher }) {
const cookie = getCookie("threads");
const response = await fetch(`https://www.threads.net/${user}/post/${id}`, {
headers: {
...commonHeaders,
cookie
},
dispatcher
});
if (cookie) updateCookie(cookie, response.headers);
if (response.status !== 200) {
return { error: "fetch.fail" };
}
const html = await response.text();
const dataString = html.match(DATA_REGEX)?.[1];
if (!dataString) {
return { error: "fetch.fail" };
}
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: "fetch.fail" };
}
const filenameBase = `threads_${post.user.username}_${post.code}`;
// Video
if (post.media_type === 2) {
if (!post.video_versions) {
return { error: "fetch.empty" };
}
// types: 640p = 101, 480p = 102, 480p-low = 103
const selectedQualityType = quality === "max" ? 101 : quality && 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: "fetch.empty" };
}
return {
urls: video.url,
filename: `${filenameBase}.mp4`,
audioFilename: `${filenameBase}_audio`
}
}
// Photo
if (post.media_type === 1) {
if (!post.image_versions2?.candidates) {
return { error: "fetch.empty" };
}
return {
urls: post.image_versions2.candidates[0].url,
filename: `${filenameBase}.jpg`,
isPhoto: true
}
}
// Mixed
if (post.media_type === 8) {
if (!post.carousel_media) {
return { error: "fetch.empty" };
}
return {
picker: post.carousel_media.map((media, i) => {
const type = media.video_versions ? "video" : "photo";
let url = media.video_versions ? media.video_versions[0].url : media.image_versions2.candidates[0].url;
const thumbProxy = createStream({
service: "threads",
type: "proxy",
u: media.image_versions2.candidates[0].url,
filename: `${filenameBase}_${i}.jpg`,
});
if (alwaysProxy) {
url = type === 'photo' ? thumbProxy : createStream({
service: "threads",
type: "proxy",
u: media.video_versions[0].url,
filename: `${filenameBase}_${i}.mp4`,
});
}
return {
type,
url,
thumb: thumbProxy
}
})
}
}
// GIPHY GIF
if (post.media_type === 19) {
if (!post.giphy_media_info?.images?.fixed_height?.webp) {
return { error: "fetch.empty" };
}
let giphyUrl = post.giphy_media_info.images.fixed_height.webp;
// In a regular browser (probably through the Accept header), the GIPHY link shows an HTML page with an ad that shows the GIF
// I'd rather have the GIF directly to save bandwidth and avoid ads.
let giphyId = giphyUrl.split("/")?.[5];
if (giphyId && !giphyId.includes(".")) giphyUrl = `https://i.giphy.com/${giphyId}.webp`;
return {
urls: giphyUrl,
filename: `${filenameBase}.webp`,
isPhoto: true
}
}
return { error: "fetch.fail" };
}