riz-ttdl 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/index.js +266 -0
- package/package.json +21 -0
package/README.md
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Made By Lenwy
|
|
3
|
+
riz-ttdl (multi fallback)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import * as cheerio from "cheerio";
|
|
8
|
+
import FormData from "form-data";
|
|
9
|
+
import qs from "qs";
|
|
10
|
+
|
|
11
|
+
const UA_MOBILE =
|
|
12
|
+
"Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Mobile Safari/537.36";
|
|
13
|
+
const UA_DESKTOP =
|
|
14
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
15
|
+
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function withRetry(fn, { retries = 1, delay = 400, debug = false, name = "" } = {}) {
|
|
21
|
+
let lastErr;
|
|
22
|
+
for (let i = 0; i <= retries; i++) {
|
|
23
|
+
try {
|
|
24
|
+
return await fn();
|
|
25
|
+
} catch (e) {
|
|
26
|
+
lastErr = e;
|
|
27
|
+
if (debug) console.error(`[riz-ttdl] ${name} attempt ${i + 1} failed:`, e?.message || e);
|
|
28
|
+
if (i < retries) await sleep(delay);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw lastErr;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function p_tikwm(url, { timeout = 20000 } = {}) {
|
|
35
|
+
const encodedParams = new URLSearchParams();
|
|
36
|
+
encodedParams.set("url", url);
|
|
37
|
+
encodedParams.set("hd", "1");
|
|
38
|
+
|
|
39
|
+
const res = await axios({
|
|
40
|
+
method: "POST",
|
|
41
|
+
url: "https://tikwm.com/api/",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
44
|
+
Cookie: "current_language=en",
|
|
45
|
+
"User-Agent": UA_MOBILE
|
|
46
|
+
},
|
|
47
|
+
data: encodedParams.toString(),
|
|
48
|
+
timeout
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const v = res?.data?.data;
|
|
52
|
+
if (!v?.play && !v?.wmplay) throw new Error("TikWM invalid response");
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
provider: "tikwm",
|
|
56
|
+
title: v.title || null,
|
|
57
|
+
cover: v.cover || null,
|
|
58
|
+
origin_cover: v.origin_cover || null,
|
|
59
|
+
no_watermark: v.play || null,
|
|
60
|
+
watermark: v.wmplay || null,
|
|
61
|
+
music: v.music || null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function p_yupra(url, { timeout = 20000, preferHd = true } = {}) {
|
|
66
|
+
const api = `https://api.yupra.my.id/api/downloader/tiktok?url=${encodeURIComponent(url)}`;
|
|
67
|
+
const res = await axios.get(api, {
|
|
68
|
+
timeout,
|
|
69
|
+
headers: { "User-Agent": UA_MOBILE }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const json = res?.data;
|
|
73
|
+
if (json?.status !== 200 || !json?.result?.data) throw new Error("Yupra invalid response");
|
|
74
|
+
|
|
75
|
+
const media = json.result.data;
|
|
76
|
+
|
|
77
|
+
const pick = (...types) =>
|
|
78
|
+
types.map((t) => media.find((x) => x.type === t)?.url).find(Boolean) || null;
|
|
79
|
+
|
|
80
|
+
const video = preferHd
|
|
81
|
+
? pick("nowatermark_hd", "nowatermark", "watermark")
|
|
82
|
+
: pick("nowatermark", "watermark", "nowatermark_hd");
|
|
83
|
+
|
|
84
|
+
if (!video) throw new Error("Yupra video not found");
|
|
85
|
+
|
|
86
|
+
const music = pick("music") || null;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
provider: "yupra",
|
|
90
|
+
title: json.result?.title || null,
|
|
91
|
+
cover: json.result?.cover || null,
|
|
92
|
+
origin_cover: json.result?.cover || null,
|
|
93
|
+
no_watermark: video,
|
|
94
|
+
watermark: video,
|
|
95
|
+
music
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function p_ssstik(url, { timeout = 25000 } = {}) {
|
|
100
|
+
const BASE = "https://ssstik.io";
|
|
101
|
+
|
|
102
|
+
const page = await axios.get(BASE, {
|
|
103
|
+
timeout,
|
|
104
|
+
headers: { "user-agent": UA_DESKTOP }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const $ = cheerio.load(page.data);
|
|
108
|
+
|
|
109
|
+
const urlPost = $('form[hx-target="#target"]').attr("hx-post");
|
|
110
|
+
const tokenJSON = $('form[hx-target="#target"]').attr("include-vals");
|
|
111
|
+
if (!urlPost || !tokenJSON) throw new Error("ssstik token not found");
|
|
112
|
+
|
|
113
|
+
const tt = tokenJSON.replace(/'/g, "").replace("tt:", "").split(",")[0];
|
|
114
|
+
const ts = tokenJSON.split("ts:")[1];
|
|
115
|
+
if (!tt || !ts) throw new Error("ssstik invalid token");
|
|
116
|
+
|
|
117
|
+
const dataPost = { id: url, locale: "en", tt, ts };
|
|
118
|
+
|
|
119
|
+
const dl = await axios.post(BASE + urlPost, qs.stringify(dataPost), {
|
|
120
|
+
timeout,
|
|
121
|
+
headers: {
|
|
122
|
+
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
123
|
+
"user-agent": UA_DESKTOP,
|
|
124
|
+
referer: BASE + "/",
|
|
125
|
+
origin: BASE
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const $$ = cheerio.load(dl.data);
|
|
130
|
+
|
|
131
|
+
const videonowm2 = $$("div > a.without_watermark_direct").attr("href") || null;
|
|
132
|
+
const videonowm = $$("div > a.without_watermark").attr("href")
|
|
133
|
+
? BASE + $$("div > a.without_watermark").attr("href")
|
|
134
|
+
: null;
|
|
135
|
+
|
|
136
|
+
const music = $$("div > a.music").attr("href") || null;
|
|
137
|
+
|
|
138
|
+
const video = videonowm2 || videonowm;
|
|
139
|
+
if (!video) throw new Error("ssstik video not found");
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
provider: "ssstik",
|
|
143
|
+
title: null,
|
|
144
|
+
cover: null,
|
|
145
|
+
origin_cover: null,
|
|
146
|
+
no_watermark: video,
|
|
147
|
+
watermark: video,
|
|
148
|
+
music
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function p_musicallydown(url, { timeout = 25000 } = {}) {
|
|
153
|
+
const base = "https://musicallydown.com";
|
|
154
|
+
|
|
155
|
+
const page = await axios.get(base, {
|
|
156
|
+
timeout,
|
|
157
|
+
headers: { "User-Agent": UA_DESKTOP }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const $ = cheerio.load(page.data);
|
|
161
|
+
|
|
162
|
+
const inputs = [];
|
|
163
|
+
$("form > div > div > input")
|
|
164
|
+
.get()
|
|
165
|
+
.forEach((el) => {
|
|
166
|
+
inputs.push({
|
|
167
|
+
name: $(el).attr("name"),
|
|
168
|
+
value: $(el).attr("value")
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (inputs.length < 3) throw new Error("musicallydown form changed");
|
|
173
|
+
|
|
174
|
+
const form = new FormData();
|
|
175
|
+
form.append(inputs[0].name, url);
|
|
176
|
+
form.append(inputs[1].name, inputs[1].value);
|
|
177
|
+
form.append(inputs[2].name, inputs[2].value);
|
|
178
|
+
|
|
179
|
+
const dl = await axios({
|
|
180
|
+
method: "POST",
|
|
181
|
+
url: base + "/download",
|
|
182
|
+
data: form,
|
|
183
|
+
timeout,
|
|
184
|
+
headers: {
|
|
185
|
+
...form.getHeaders(),
|
|
186
|
+
"User-Agent": UA_DESKTOP,
|
|
187
|
+
origin: base,
|
|
188
|
+
referer: base + "/"
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const $$ = cheerio.load(dl.data);
|
|
193
|
+
|
|
194
|
+
const direct = $$("a")
|
|
195
|
+
.toArray()
|
|
196
|
+
.map((a) => $$(a).attr("href"))
|
|
197
|
+
.find((h) => h && /^https?:\/\//.test(h) && (h.includes(".mp4") || h.includes("download")));
|
|
198
|
+
|
|
199
|
+
const title = $$("div.row > div > h2 > b").text() || null;
|
|
200
|
+
const preview = $$("video#video").attr("poster") || null;
|
|
201
|
+
|
|
202
|
+
if (!direct) throw new Error("musicallydown direct not found");
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
provider: "musicallydown",
|
|
206
|
+
title,
|
|
207
|
+
cover: preview,
|
|
208
|
+
origin_cover: preview,
|
|
209
|
+
no_watermark: direct,
|
|
210
|
+
watermark: direct,
|
|
211
|
+
music: null
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function tiktok2(url, options = {}) {
|
|
216
|
+
if (!url) throw new Error("URL TikTok wajib diisi");
|
|
217
|
+
|
|
218
|
+
const {
|
|
219
|
+
timeout = 20000,
|
|
220
|
+
retries = 1,
|
|
221
|
+
debug = false,
|
|
222
|
+
preferHd = true,
|
|
223
|
+
order = ["tikwm", "yupra", "ssstik", "musicallydown"]
|
|
224
|
+
} = options;
|
|
225
|
+
|
|
226
|
+
const providers = {
|
|
227
|
+
tikwm: () => p_tikwm(url, { timeout }),
|
|
228
|
+
yupra: () => p_yupra(url, { timeout, preferHd }),
|
|
229
|
+
ssstik: () => p_ssstik(url, { timeout: Math.max(timeout, 25000) }),
|
|
230
|
+
musicallydown: () => p_musicallydown(url, { timeout: Math.max(timeout, 25000) })
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const errors = {};
|
|
234
|
+
|
|
235
|
+
for (const name of order) {
|
|
236
|
+
const runner = providers[name];
|
|
237
|
+
if (!runner) continue;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const result = await withRetry(runner, {
|
|
241
|
+
retries,
|
|
242
|
+
delay: 500,
|
|
243
|
+
debug,
|
|
244
|
+
name
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
provider: result.provider || name,
|
|
249
|
+
title: result.title || null,
|
|
250
|
+
cover: result.cover || null,
|
|
251
|
+
origin_cover: result.origin_cover || null,
|
|
252
|
+
no_watermark: result.no_watermark || null,
|
|
253
|
+
watermark: result.watermark || null,
|
|
254
|
+
music: result.music || null
|
|
255
|
+
};
|
|
256
|
+
} catch (e) {
|
|
257
|
+
errors[name] = e?.message || String(e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const err = new Error("Semua provider gagal");
|
|
262
|
+
err.cause = errors;
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export default tiktok2;
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "riz-ttdl",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "TikTok downloader multi fallback (tikwm, yupra, ssstik, musicallydown)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"tiktok",
|
|
9
|
+
"downloader",
|
|
10
|
+
"ttdl",
|
|
11
|
+
"nowatermark"
|
|
12
|
+
],
|
|
13
|
+
"author": "Rizky",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"axios": "^1.13.2",
|
|
17
|
+
"cheerio": "^1.1.2",
|
|
18
|
+
"form-data": "^4.0.5",
|
|
19
|
+
"qs": "^6.14.0"
|
|
20
|
+
}
|
|
21
|
+
}
|