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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/index.js +266 -0
  3. package/package.json +21 -0
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # riz-ttdl
2
+
3
+ TikTok downloader multi fallback.
4
+
5
+ ## Install
6
+ npm i riz-ttdl
7
+
8
+ ## Usage
9
+ import tiktok2 from "riz-ttdl"
10
+
11
+ const data = await tiktok2("https://vt.tiktok.com/xxxxx", { retries: 2 })
12
+
13
+ console.log(data.no_watermark)
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
+ }