@paiavaliable/scrape-lib 1.0.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 +103 -0
- package/index.js +16 -0
- package/lib/aioDownload.js +42 -0
- package/lib/bypassLink.js +40 -0
- package/lib/hdImage.js +50 -0
- package/lib/searchSongs.js +158 -0
- package/lib/toHitam.js +48 -0
- package/lib/ytmp3.js +86 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @yourname/scrape-lib
|
|
2
|
+
|
|
3
|
+
> Ganti `@yourname` di `package.json` dengan scope npm lo sendiri sebelum publish.
|
|
4
|
+
|
|
5
|
+
Kumpulan scraper/downloader utility buat WhatsApp bot / project Node.js lain.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @yourname/scrape-lib
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const scrape = require('@yourname/scrape-lib');
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `searchSongs(query, opts?)`
|
|
20
|
+
Cari lagu di YouTube Music.
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
const list = await scrape.searchSongs('love story');
|
|
24
|
+
console.log(list);
|
|
25
|
+
// [{ type, videoId, url, name, artist, album, duration, thumbnails }, ...]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`opts`: `{ GL, HL, cookies }` (opsional — region/language code & cookie string).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### `ytmp3(youtubeUrl, format?)`
|
|
33
|
+
Convert video YouTube ke mp3/mp4.
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const res = await scrape.ytmp3('https://youtu.be/xxxxxxxxxxx', 'mp3');
|
|
37
|
+
console.log(res.downloadURL);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`format` default `'mp3'`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### `aioDownload(url)`
|
|
45
|
+
Download media dari TikTok, Twitter, FB, IG, Pinterest, LinkedIn, Snapchat, Threads, Tumblr.
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const res = await scrape.aioDownload('https://vt.tiktok.com/xxxxxxxx/');
|
|
49
|
+
console.log(res);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### `bypassLink(url)`
|
|
55
|
+
Bypass link shortener / ad-wall (Linkvertise, dll).
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const res = await scrape.bypassLink('https://linkvertise.com/xxxxxxx');
|
|
59
|
+
console.log(res);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### `hdImage(input, opts?)`
|
|
65
|
+
Upscale gambar ke HD.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
// dari path file lokal
|
|
69
|
+
const res = await scrape.hdImage('./image.png');
|
|
70
|
+
|
|
71
|
+
// dari Buffer (misal media yang di-download dari WA)
|
|
72
|
+
const buffer = await someMessage.download();
|
|
73
|
+
const res = await scrape.hdImage(buffer, { filename: 'photo.jpg' });
|
|
74
|
+
|
|
75
|
+
console.log(res.result);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### `toHitam(input)`
|
|
81
|
+
Filter gambar jadi hitam-putih.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
// dari URL
|
|
85
|
+
const res = await scrape.toHitam('https://example.com/foto.jpg');
|
|
86
|
+
|
|
87
|
+
// dari Buffer
|
|
88
|
+
const res2 = await scrape.toHitam({ buffer: mediaBuffer, mimetype: 'image/jpeg' });
|
|
89
|
+
|
|
90
|
+
console.log(res.url); // URL gambar hasil filter
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Catatan
|
|
96
|
+
|
|
97
|
+
Semua fungsi di sini nge-hit API/service pihak ketiga (bukan self-hosted), jadi:
|
|
98
|
+
- Bisa aja berhenti kerja kalo service-nya berubah/down — bukan bug di package ini.
|
|
99
|
+
- Jangan spam request berlebihan ke service-service tersebut.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { searchSongs } = require('./lib/searchSongs');
|
|
2
|
+
const { ytmp3 } = require('./lib/ytmp3');
|
|
3
|
+
const { aioDownload } = require('./lib/aioDownload');
|
|
4
|
+
const { getToken, bypassLink } = require('./lib/bypassLink');
|
|
5
|
+
const { hdImage } = require('./lib/hdImage');
|
|
6
|
+
const { toHitam } = require('./lib/toHitam');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
searchSongs,
|
|
10
|
+
ytmp3,
|
|
11
|
+
aioDownload,
|
|
12
|
+
getToken,
|
|
13
|
+
bypassLink,
|
|
14
|
+
hdImage,
|
|
15
|
+
toHitam
|
|
16
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ALL-IN-ONE DOWNLOADER
|
|
3
|
+
* base : https://getindevice.com
|
|
4
|
+
* support : TikTok, Twitter, FB, IG, Pinterest, LinkedIn, Snapchat, Threads, Tumblr
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const axios = require("axios");
|
|
8
|
+
|
|
9
|
+
const headers = {
|
|
10
|
+
"user-agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
|
|
11
|
+
"referer": "https://getindevice.com/"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function aioDownload(url) {
|
|
15
|
+
try {
|
|
16
|
+
const { data: token } = await axios.get(
|
|
17
|
+
`https://getindevice.com/api/token/?_t=${Date.now()}`,
|
|
18
|
+
{ headers }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const { data } = await axios.post(
|
|
22
|
+
"https://getindevice.com/api/download/",
|
|
23
|
+
{ url },
|
|
24
|
+
{
|
|
25
|
+
headers: {
|
|
26
|
+
...headers,
|
|
27
|
+
"content-type": "application/json",
|
|
28
|
+
"x-request-token": token.token
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return data;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
status: false,
|
|
37
|
+
message: error.response?.data || error.message
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { aioDownload };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINK BYPASS
|
|
3
|
+
* base : https://bypass-links.com
|
|
4
|
+
* Bypasses link shorteners / ad-walls (Linkvertise, etc).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const axios = require("axios");
|
|
8
|
+
|
|
9
|
+
const HEADERS = {
|
|
10
|
+
accept: "*/*",
|
|
11
|
+
"accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
12
|
+
"cache-control": "no-cache",
|
|
13
|
+
pragma: "no-cache",
|
|
14
|
+
"sec-ch-ua": "\"Mises\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
|
|
15
|
+
"sec-ch-ua-mobile": "?1",
|
|
16
|
+
"sec-ch-ua-platform": "\"Android\"",
|
|
17
|
+
"sec-fetch-dest": "empty",
|
|
18
|
+
"sec-fetch-mode": "cors",
|
|
19
|
+
"sec-fetch-site": "same-origin",
|
|
20
|
+
Referer: "https://bypass-links.com/"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function getToken() {
|
|
24
|
+
const res = await axios.get("https://bypass-links.com/api/token", {
|
|
25
|
+
headers: HEADERS
|
|
26
|
+
});
|
|
27
|
+
return res.data.token;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function bypassLink(url) {
|
|
31
|
+
const bypass_token = await getToken();
|
|
32
|
+
const res = await axios.post(
|
|
33
|
+
"https://bypass-links.com/api/bypass",
|
|
34
|
+
{ url, bypass_token },
|
|
35
|
+
{ headers: { ...HEADERS, "content-type": "application/json" } }
|
|
36
|
+
);
|
|
37
|
+
return res.data;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { getToken, bypassLink };
|
package/lib/hdImage.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HD IMAGE UPSCALER
|
|
3
|
+
* base : https://dayydl.dpdns.org
|
|
4
|
+
* Upscales/enhances an image to HD via the dayydl.dpdns.org API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const axios = require("axios");
|
|
8
|
+
const FormData = require("form-data");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string|Buffer} input - path to a local file, or a Buffer with image data
|
|
13
|
+
* @param {object} [opts]
|
|
14
|
+
* @param {string} [opts.filename] - filename to use when input is a Buffer
|
|
15
|
+
*/
|
|
16
|
+
async function hdImage(input, opts = {}) {
|
|
17
|
+
try {
|
|
18
|
+
const form = new FormData();
|
|
19
|
+
|
|
20
|
+
if (Buffer.isBuffer(input)) {
|
|
21
|
+
form.append("image", input, opts.filename || "image.png");
|
|
22
|
+
} else {
|
|
23
|
+
if (!fs.existsSync(input)) {
|
|
24
|
+
return { success: false, error: "File not found" };
|
|
25
|
+
}
|
|
26
|
+
form.append("image", fs.createReadStream(input));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { data } = await axios.post("https://dayydl.dpdns.org/api/hd/image", form, {
|
|
30
|
+
headers: {
|
|
31
|
+
...form.getHeaders(),
|
|
32
|
+
"accept": "*/*",
|
|
33
|
+
"alt-used": "dayydl.dpdns.org",
|
|
34
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0"
|
|
35
|
+
},
|
|
36
|
+
timeout: 300000
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (data) {
|
|
40
|
+
return { success: true, result: data };
|
|
41
|
+
} else {
|
|
42
|
+
return { success: false, error: "Empty response from server" };
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const errorMsg = error.response && error.response.data ? error.response.data : error.message;
|
|
46
|
+
return { success: false, error: errorMsg };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { hdImage };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YOUTUBE MUSIC FINDER
|
|
3
|
+
* Search songs via YouTube Music's internal `youtubei` API.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
async function searchSongs(query, { GL, HL, cookies } = {}) {
|
|
7
|
+
const BASE = "https://music.youtube.com/";
|
|
8
|
+
const cookieMap = new Map();
|
|
9
|
+
|
|
10
|
+
const saveCookies = res => {
|
|
11
|
+
for (const raw of res.headers.getSetCookie?.() ?? []) {
|
|
12
|
+
const [pair] = raw.split(";");
|
|
13
|
+
const [name, ...rest] = pair.split("=");
|
|
14
|
+
if (name) cookieMap.set(name.trim(), rest.join("=").trim());
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const cookieHeader = () =>
|
|
19
|
+
[...cookieMap.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
20
|
+
|
|
21
|
+
const HEADERS = {
|
|
22
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 16; Infinix X6837 Build/BP2A.250605.031.A2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.217 Mobile Safari/537.36",
|
|
23
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
24
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
25
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (cookies) {
|
|
29
|
+
for (const part of cookies.split("; ")) {
|
|
30
|
+
const [name, ...rest] = part.split("=");
|
|
31
|
+
cookieMap.set(name.trim(), rest.join("="));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const homeRes = await fetch(BASE, {
|
|
36
|
+
headers: { ...HEADERS, cookie: cookieHeader() },
|
|
37
|
+
redirect: "follow",
|
|
38
|
+
});
|
|
39
|
+
saveCookies(homeRes);
|
|
40
|
+
|
|
41
|
+
if (!homeRes.ok) throw new Error(`Homepage fetch failed: ${homeRes.status}`);
|
|
42
|
+
|
|
43
|
+
const html = await homeRes.text();
|
|
44
|
+
if (!html.includes("ytcfg")) throw new Error("ytcfg not found");
|
|
45
|
+
|
|
46
|
+
const config = {};
|
|
47
|
+
for (const m of html.matchAll(/ytcfg\.set\(({.+?})\)/g)) {
|
|
48
|
+
try { Object.assign(config, JSON.parse(m[1])); } catch {}
|
|
49
|
+
}
|
|
50
|
+
if (GL) config.GL = GL;
|
|
51
|
+
if (HL) config.HL = HL;
|
|
52
|
+
|
|
53
|
+
if (!config.INNERTUBE_API_KEY) throw new Error("Gagal ambil INNERTUBE_API_KEY");
|
|
54
|
+
|
|
55
|
+
const params = new URLSearchParams({ alt: "json", key: config.INNERTUBE_API_KEY });
|
|
56
|
+
const searchRes = await fetch(`${BASE}youtubei/${config.INNERTUBE_API_VERSION}/search?${params}`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
...HEADERS,
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"x-origin": BASE,
|
|
62
|
+
"X-Goog-Visitor-Id": config.VISITOR_DATA || "",
|
|
63
|
+
"X-YouTube-Client-Name": String(config.INNERTUBE_CONTEXT_CLIENT_NAME),
|
|
64
|
+
"X-YouTube-Client-Version": config.INNERTUBE_CLIENT_VERSION,
|
|
65
|
+
"X-YouTube-Utc-Offset": String(-new Date().getTimezoneOffset()),
|
|
66
|
+
"X-YouTube-Time-Zone": Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
67
|
+
"cookie": cookieHeader(),
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
context: {
|
|
71
|
+
capabilities: {},
|
|
72
|
+
client: {
|
|
73
|
+
clientName: config.INNERTUBE_CLIENT_NAME,
|
|
74
|
+
clientVersion: config.INNERTUBE_CLIENT_VERSION,
|
|
75
|
+
experimentIds: [],
|
|
76
|
+
experimentsToken: "",
|
|
77
|
+
gl: config.GL,
|
|
78
|
+
hl: config.HL,
|
|
79
|
+
locationInfo: { locationPermissionAuthorizationStatus: "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED" },
|
|
80
|
+
musicAppInfo: {
|
|
81
|
+
musicActivityMasterSwitch: "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE",
|
|
82
|
+
musicLocationMasterSwitch: "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE",
|
|
83
|
+
pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_UNKNOWN",
|
|
84
|
+
},
|
|
85
|
+
utcOffsetMinutes: -new Date().getTimezoneOffset(),
|
|
86
|
+
},
|
|
87
|
+
request: {
|
|
88
|
+
internalExperimentFlags: [
|
|
89
|
+
{ key: "force_music_enable_outertube_tastebuilder_browse", value: "true" },
|
|
90
|
+
{ key: "force_music_enable_outertube_playlist_detail_browse", value: "true" },
|
|
91
|
+
{ key: "force_music_enable_outertube_search_suggestions", value: "true" },
|
|
92
|
+
],
|
|
93
|
+
sessionIndex: {},
|
|
94
|
+
},
|
|
95
|
+
user: { enableSafetyMode: false },
|
|
96
|
+
},
|
|
97
|
+
query,
|
|
98
|
+
params: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D",
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!searchRes.ok) throw new Error(`Search request failed: ${searchRes.status}`);
|
|
103
|
+
|
|
104
|
+
const data = await searchRes.json();
|
|
105
|
+
|
|
106
|
+
const traverse = (data, ...keys) => {
|
|
107
|
+
const again = (data, key, deadEnd = false) => {
|
|
108
|
+
const res = [];
|
|
109
|
+
if (data instanceof Object && key in data) {
|
|
110
|
+
res.push(data[key]);
|
|
111
|
+
if (deadEnd) return res.length === 1 ? res[0] : res;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(data)) res.push(...data.map(v => again(v, key)).flat());
|
|
114
|
+
else if (data instanceof Object) res.push(...Object.keys(data).map(k => again(data[k], key)).flat());
|
|
115
|
+
return res.length === 1 ? res[0] : res;
|
|
116
|
+
};
|
|
117
|
+
let value = data;
|
|
118
|
+
const lastKey = keys.at(-1);
|
|
119
|
+
for (const key of keys) value = again(value, key, lastKey === key);
|
|
120
|
+
return value;
|
|
121
|
+
};
|
|
122
|
+
const traverseList = (data, ...keys) => [traverse(data, ...keys)].flat();
|
|
123
|
+
const traverseString = (data, ...keys) => traverseList(data, ...keys).at(0) ?? "";
|
|
124
|
+
const parseDuration = time => {
|
|
125
|
+
if (!time) return null;
|
|
126
|
+
const [s, m, h] = time.split(":").reverse().map(Number);
|
|
127
|
+
return (s || 0) + (m || 0) * 60 + (h || 0) * 3600;
|
|
128
|
+
};
|
|
129
|
+
const isArtist = d => ["MUSIC_PAGE_TYPE_USER_CHANNEL", "MUSIC_PAGE_TYPE_ARTIST"].includes(traverseString(d, "pageType"));
|
|
130
|
+
const isAlbum = d => traverseString(d, "pageType") === "MUSIC_PAGE_TYPE_ALBUM";
|
|
131
|
+
const isDuration = d => traverseString(d, "text").match(/(\d{1,2}:)?\d{1,2}:\d{1,2}/);
|
|
132
|
+
|
|
133
|
+
return traverseList(data, "musicResponsiveListItemRenderer").map(item => {
|
|
134
|
+
const columns = traverseList(item, "flexColumns", "runs");
|
|
135
|
+
const title = columns[0];
|
|
136
|
+
const artist = columns.find(isArtist) ?? columns[3];
|
|
137
|
+
const album = columns.find(isAlbum) ?? null;
|
|
138
|
+
const duration = columns.find(isDuration);
|
|
139
|
+
return {
|
|
140
|
+
type: "SONG",
|
|
141
|
+
videoId: traverseString(item, "playlistItemData", "videoId"),
|
|
142
|
+
url: "https://music.youtube.com/watch?v=" + traverseString(item, "playlistItemData", "videoId"),
|
|
143
|
+
name: traverseString(title, "text"),
|
|
144
|
+
artist: {
|
|
145
|
+
name: traverseString(artist, "text"),
|
|
146
|
+
artistId: traverseString(artist, "browseId") || null,
|
|
147
|
+
},
|
|
148
|
+
album: album ? {
|
|
149
|
+
name: traverseString(album, "text"),
|
|
150
|
+
albumId: traverseString(album, "browseId"),
|
|
151
|
+
} : null,
|
|
152
|
+
duration: parseDuration(duration?.text),
|
|
153
|
+
thumbnails: traverseList(item, "thumbnails"),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { searchSongs };
|
package/lib/toHitam.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLACK & WHITE IMAGE FILTER
|
|
3
|
+
* base : https://api.cuki.biz.id (nanobanana), https://api.shinzu.web.id (temp host)
|
|
4
|
+
*
|
|
5
|
+
* Note: extracted from a WhatsApp-bot plugin handler and stripped of all
|
|
6
|
+
* bot-specific reply/react logic. Only the upload + filter API calls remain.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const axios = require("axios");
|
|
10
|
+
const FormData = require("form-data");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string|{buffer: Buffer, mimetype?: string, filename?: string}} input
|
|
14
|
+
* Either a direct image URL, or a Buffer (with optional mimetype/filename)
|
|
15
|
+
* that will be uploaded to a temp host first.
|
|
16
|
+
* @returns {Promise<{url: string, sourceUrl: string}>}
|
|
17
|
+
*/
|
|
18
|
+
async function toHitam(input) {
|
|
19
|
+
let url = input;
|
|
20
|
+
|
|
21
|
+
if (input && typeof input === "object" && Buffer.isBuffer(input.buffer)) {
|
|
22
|
+
const form = new FormData();
|
|
23
|
+
form.append("file", input.buffer, {
|
|
24
|
+
filename: input.filename || "upload_file",
|
|
25
|
+
contentType: input.mimetype || "image/jpeg"
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const uploadRes = await axios.post("https://api.shinzu.web.id/api/upload/litterbox", form, {
|
|
29
|
+
headers: form.getHeaders()
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!uploadRes.data?.status || !uploadRes.data?.result?.url) {
|
|
33
|
+
throw new Error("Gagal mengunggah gambar ke server uploader.");
|
|
34
|
+
}
|
|
35
|
+
url = uploadRes.data.result.url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!url || typeof url !== "string") {
|
|
39
|
+
throw new Error("Butuh URL gambar (string) atau { buffer, mimetype }.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sourceUrl = url.trim();
|
|
43
|
+
const apiUrl = `https://api.cuki.biz.id/api/nanobanana/tohitam?apikey=cuki-x&url=${encodeURIComponent(sourceUrl)}`;
|
|
44
|
+
|
|
45
|
+
return { url: apiUrl, sourceUrl };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { toHitam };
|
package/lib/ytmp3.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YOUTUBE MP3 DOWNLOADER
|
|
3
|
+
* Converts a YouTube video to mp3/mp4 via the etacloud.org backend.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
async function ytmp3(youtubeUrl, format = 'mp3') {
|
|
7
|
+
const match = /(?:youtu\.be\/|youtube\.com\/(?:embed\/|live\/|shorts\/)|[?&]v=)([a-zA-Z0-9-_]{11})/.exec(youtubeUrl);
|
|
8
|
+
if (!match) return { message: "URL YouTube tidak valid" };
|
|
9
|
+
|
|
10
|
+
const videoId = match[1];
|
|
11
|
+
const endpoint = 'etacloud.org';
|
|
12
|
+
const getTs = () => Date.now();
|
|
13
|
+
|
|
14
|
+
const browserHeaders = {
|
|
15
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 16; Infinix X6837 Build/BP2A.250605.031.A2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.217 Mobile Safari/537.36",
|
|
16
|
+
"Accept": "*/*",
|
|
17
|
+
"Origin": "https://y2mate.cc",
|
|
18
|
+
"Referer": "https://y2mate.cc/",
|
|
19
|
+
"X-Requested-With": "com.xbrowser.play",
|
|
20
|
+
"Sec-Fetch-Site": "cross-site",
|
|
21
|
+
"Sec-Fetch-Mode": "cors"
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function safeFetch(url, options = {}) {
|
|
25
|
+
options.headers = { ...browserHeaders, ...options.headers };
|
|
26
|
+
const res = await fetch(url, options);
|
|
27
|
+
const text = await res.text();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error(`Diblokir oleh server! URL: ${url} \nStatus HTTP: ${res.status}\nBody Response: ${text.substring(0, 150)}...`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const appendParam = (url, params) => url.includes('?') ? `${url}&${params}` : `${url}?${params}`;
|
|
37
|
+
try {
|
|
38
|
+
const authData = await safeFetch(`https://eta.${endpoint}/api/v1/auth?_=${getTs()}`);
|
|
39
|
+
if (authData.error > 0) throw new Error('Authorization gagal');
|
|
40
|
+
|
|
41
|
+
const initData = await safeFetch(`https://eta.${endpoint}/api/v1/init?_=${getTs()}`, {
|
|
42
|
+
headers: { 'Authorization': `Bearer ${authData.key}` }
|
|
43
|
+
});
|
|
44
|
+
if (initData.error > 0) throw new Error('Initialization gagal');
|
|
45
|
+
|
|
46
|
+
async function executeConvert(url) {
|
|
47
|
+
let baseUrl = url.includes('&v=') ? url.split('&v=')[0] : url;
|
|
48
|
+
|
|
49
|
+
let fetchUrl = appendParam(baseUrl, `v=${videoId}&f=${format}&_=${getTs()}`);
|
|
50
|
+
const convertData = await safeFetch(fetchUrl);
|
|
51
|
+
|
|
52
|
+
if (convertData.error > 0) throw new Error(`Server menolak konversi. Error Code: ${convertData.error}`);
|
|
53
|
+
|
|
54
|
+
if (convertData.redirect === 1) {
|
|
55
|
+
return executeConvert(convertData.redirectURL);
|
|
56
|
+
}
|
|
57
|
+
return convertData;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const convertData = await executeConvert(initData.convertURL);
|
|
61
|
+
let finalData = convertData;
|
|
62
|
+
|
|
63
|
+
while (finalData.progress !== undefined && finalData.progress < 3) {
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
65
|
+
let progressUrl = appendParam(convertData.progressURL, `_=${getTs()}`);
|
|
66
|
+
finalData = await safeFetch(progressUrl);
|
|
67
|
+
|
|
68
|
+
if (finalData.error > 0) throw new Error('Server gagal saat mencoba men-convert video');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
videoId: videoId,
|
|
73
|
+
format: format,
|
|
74
|
+
title: finalData.title || convertData.title || "",
|
|
75
|
+
downloadURL: finalData.downloadURL || convertData.downloadURL,
|
|
76
|
+
raw: finalData
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
message: error.message
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { ytmp3 };
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paiavaliable/scrape-lib",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Kumpulan scraper/downloader utility: YT Music search, YT downloader, AIO social media downloader, link bypass, HD image upscaler, black & white image filter.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node example/test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"scraper",
|
|
15
|
+
"downloader",
|
|
16
|
+
"whatsapp-bot",
|
|
17
|
+
"youtube",
|
|
18
|
+
"tiktok",
|
|
19
|
+
"instagram"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"axios": "^1.7.2",
|
|
25
|
+
"form-data": "^4.0.0"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"index.js",
|
|
29
|
+
"lib/"
|
|
30
|
+
]
|
|
31
|
+
}
|