indo-scraper 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 +646 -0
- package/index.js +38 -0
- package/indo-scraper.zip +0 -0
- package/package.json +26 -0
- package/src/bmkg/cuaca.js +34 -0
- package/src/bmkg/gempa.js +56 -0
- package/src/downloader/facebook.js +94 -0
- package/src/downloader/gdrive.js +38 -0
- package/src/downloader/instagram.js +62 -0
- package/src/downloader/mediafire.js +30 -0
- package/src/downloader/spotify.js +262 -0
- package/src/downloader/tiktok.js +472 -0
- package/src/finance/bbm.js +51 -0
- package/src/finance/emas.js +46 -0
- package/src/finance/kurs.js +64 -0
- package/src/finance/saham.js +117 -0
- package/src/info/cekno.js +39 -0
- package/src/info/resi.js +82 -0
- package/src/news/antara.js +66 -0
- package/src/news/cnn.js +71 -0
- package/src/news/detik.js +108 -0
- package/src/news/kompas.js +70 -0
- package/src/news/liputan6.js +65 -0
- package/src/news/okezone.js +72 -0
- package/src/news/republika.js +73 -0
- package/src/news/tribun.js +95 -0
- package/src/tools/simsimi.js +69 -0
- package/src/tools/ssweb.js +35 -0
- package/src/utils.js +79 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { fetchJSON, ok, fail } = require('../utils')
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Prakiraan cuaca kota populer
|
|
5
|
+
* @param {string} kota - jakarta | bandung | surabaya | medan | semarang | makassar | yogyakarta | palembang | denpasar | balikpapan
|
|
6
|
+
*/
|
|
7
|
+
const bmkgCuaca = async (kota = 'jakarta') => {
|
|
8
|
+
return new Promise(async (resolve) => {
|
|
9
|
+
try {
|
|
10
|
+
const KOTA = {
|
|
11
|
+
jakarta: '31.71.05.1001', bandung: '32.73.04.1002', surabaya: '35.78.27.1001',
|
|
12
|
+
medan: '12.71.01.1001', semarang: '33.74.01.1001', makassar: '73.71.01.1001',
|
|
13
|
+
yogyakarta: '34.71.01.1001', palembang: '16.71.01.1001',
|
|
14
|
+
denpasar: '51.71.01.1001', balikpapan: '64.72.01.1001',
|
|
15
|
+
}
|
|
16
|
+
const adm4 = KOTA[kota.toLowerCase()]
|
|
17
|
+
if (!adm4) return resolve(fail(`Kota tidak tersedia. Pilihan: ${Object.keys(KOTA).join(', ')}`))
|
|
18
|
+
const json = await fetchJSON(`https://api.bmkg.go.id/publik/prakiraan-cuaca?adm4=${adm4}`)
|
|
19
|
+
const lokasi = json.data?.[0]?.lokasi || {}
|
|
20
|
+
const cuaca = (json.data?.[0]?.cuaca || []).flat().map(i => ({
|
|
21
|
+
datetime: i.local_datetime, cuaca: i.weather_desc,
|
|
22
|
+
suhu: i.t, suhu_min: i.tmin, suhu_max: i.tmax,
|
|
23
|
+
kelembaban: i.hu, angin: i.ws, arah_angin: i.wd, icon: i.image,
|
|
24
|
+
}))
|
|
25
|
+
if (!cuaca.length) return resolve(fail('Data cuaca tidak ditemukan'))
|
|
26
|
+
resolve(ok({
|
|
27
|
+
lokasi: { kecamatan: lokasi.kecamatan, kotkab: lokasi.kotkab, provinsi: lokasi.provinsi },
|
|
28
|
+
prakiraan: cuaca,
|
|
29
|
+
}))
|
|
30
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { bmkgCuaca }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { fetchJSON, ok, fail } = require('../utils')
|
|
2
|
+
|
|
3
|
+
/* Gempa terbaru (1 data) */
|
|
4
|
+
const bmkgGempa = async () => {
|
|
5
|
+
return new Promise(async (resolve) => {
|
|
6
|
+
try {
|
|
7
|
+
const json = await fetchJSON('https://data.bmkg.go.id/DataMKG/TEWS/autogempa.json')
|
|
8
|
+
const g = json.Infogempa.gempa
|
|
9
|
+
resolve(ok({
|
|
10
|
+
tanggal: g.Tanggal, jam: g.Jam, datetime: g.DateTime,
|
|
11
|
+
koordinat: g.Coordinates, lintang: g.Lintang, bujur: g.Bujur,
|
|
12
|
+
magnitude: g.Magnitude, kedalaman: g.Kedalaman,
|
|
13
|
+
wilayah: g.Wilayah, potensi: g.Potensi, dirasakan: g.Dirasakan,
|
|
14
|
+
shakemap: `https://data.bmkg.go.id/DataMKG/TEWS/${g.Shakemap}`,
|
|
15
|
+
}))
|
|
16
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* 15 gempa terkini */
|
|
21
|
+
const bmkgGempaTerkini = async () => {
|
|
22
|
+
return new Promise(async (resolve) => {
|
|
23
|
+
try {
|
|
24
|
+
const json = await fetchJSON('https://data.bmkg.go.id/DataMKG/TEWS/gempaterkini.json')
|
|
25
|
+
const list = json.Infogempa.gempa
|
|
26
|
+
if (!list?.length) return resolve(fail('Data tidak ditemukan'))
|
|
27
|
+
resolve(ok(list.map(g => ({
|
|
28
|
+
tanggal: g.Tanggal, jam: g.Jam, datetime: g.DateTime,
|
|
29
|
+
koordinat: g.Coordinates, lintang: g.Lintang, bujur: g.Bujur,
|
|
30
|
+
magnitude: g.Magnitude, kedalaman: g.Kedalaman,
|
|
31
|
+
wilayah: g.Wilayah, potensi: g.Potensi,
|
|
32
|
+
shakemap: `https://data.bmkg.go.id/DataMKG/TEWS/${g.Shakemap}`,
|
|
33
|
+
}))))
|
|
34
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Gempa yang dirasakan */
|
|
39
|
+
const bmkgGempaDirasakan = async () => {
|
|
40
|
+
return new Promise(async (resolve) => {
|
|
41
|
+
try {
|
|
42
|
+
const json = await fetchJSON('https://data.bmkg.go.id/DataMKG/TEWS/gempadirasakan.json')
|
|
43
|
+
const list = json.Infogempa.gempa
|
|
44
|
+
if (!list?.length) return resolve(fail('Data tidak ditemukan'))
|
|
45
|
+
resolve(ok(list.map(g => ({
|
|
46
|
+
tanggal: g.Tanggal, jam: g.Jam, datetime: g.DateTime,
|
|
47
|
+
koordinat: g.Coordinates, lintang: g.Lintang, bujur: g.Bujur,
|
|
48
|
+
magnitude: g.Magnitude, kedalaman: g.Kedalaman,
|
|
49
|
+
wilayah: g.Wilayah, dirasakan: g.Dirasakan, potensi: g.Potensi,
|
|
50
|
+
shakemap: `https://data.bmkg.go.id/DataMKG/TEWS/${g.Shakemap}`,
|
|
51
|
+
}))))
|
|
52
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { bmkgGempa, bmkgGempaTerkini, bmkgGempaDirasakan }
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const { axios, cheerio, ok, fail } = require('../utils')
|
|
2
|
+
|
|
3
|
+
const _getTitle = async (url) => {
|
|
4
|
+
try {
|
|
5
|
+
const res = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Linux; Android 13)', 'Accept-Language': 'id-ID,id;q=0.9' }, timeout: 15000 })
|
|
6
|
+
const $ = cheerio.load(res.data)
|
|
7
|
+
return ($('meta[property="og:description"]').attr('content') ||
|
|
8
|
+
$('meta[name="description"]').attr('content') ||
|
|
9
|
+
$('meta[property="og:title"]').attr('content') ||
|
|
10
|
+
$('title').text() || '')
|
|
11
|
+
.replace(/\| Facebook.*$/i, '').replace(/&/g, '&').replace(/\s+/g, ' ').trim()
|
|
12
|
+
} catch (_) { return '' }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Facebook Video Downloader
|
|
17
|
+
* Source: fdown.net
|
|
18
|
+
* @param {string} url
|
|
19
|
+
*/
|
|
20
|
+
const facebook = async (url) => {
|
|
21
|
+
return new Promise(async (resolve) => {
|
|
22
|
+
try {
|
|
23
|
+
if (!url) return resolve(fail('URL tidak boleh kosong'))
|
|
24
|
+
if (!url.includes('facebook.com') && !url.includes('fb.watch'))
|
|
25
|
+
return resolve(fail('URL Facebook tidak valid'))
|
|
26
|
+
|
|
27
|
+
const title = await _getTitle(url) || 'Facebook Video'
|
|
28
|
+
|
|
29
|
+
const res = await axios.post(
|
|
30
|
+
'https://fdown.net/download.php',
|
|
31
|
+
new URLSearchParams({ URLz: url }).toString(),
|
|
32
|
+
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Linux; Android 13)', 'Origin': 'https://fdown.net', 'Referer': 'https://fdown.net/' }, timeout: 20000 }
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const $ = cheerio.load(res.data)
|
|
36
|
+
const thumbnail = $('.lib-item img').attr('src') || $('img').first().attr('src') || null
|
|
37
|
+
let sd = null, hd = null
|
|
38
|
+
|
|
39
|
+
$('a').each((_, el) => {
|
|
40
|
+
const href = $(el).attr('href')
|
|
41
|
+
const text = $(el).text().trim()
|
|
42
|
+
if (!href) return
|
|
43
|
+
if (text.includes('Download Video in HD Quality')) hd = href
|
|
44
|
+
if (text.includes('Download Video in Normal Quality')) sd = href
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!sd) sd = res.data.match(/Download Video in Normal Quality.*?href="(.*?)"/s)?.[1] || null
|
|
48
|
+
if (!hd) hd = res.data.match(/Download Video in HD Quality.*?href="(.*?)"/s)?.[1] || null
|
|
49
|
+
if (!sd && !hd) return resolve(fail('Video tidak ditemukan atau private'))
|
|
50
|
+
|
|
51
|
+
resolve(ok({ title, thumbnail, sd, hd: hd || sd }))
|
|
52
|
+
} catch (e) { console.log(e); resolve(fail(e?.response?.status || e.message)) }
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* Facebook Photo Downloader
|
|
58
|
+
* Support: post photo, album, carousel, share link
|
|
59
|
+
* @param {string} url
|
|
60
|
+
*/
|
|
61
|
+
const facebookPhoto = async (url) => {
|
|
62
|
+
return new Promise(async (resolve) => {
|
|
63
|
+
try {
|
|
64
|
+
if (!url) return resolve(fail('URL tidak boleh kosong'))
|
|
65
|
+
if (!url.includes('facebook.com') && !url.includes('fb.watch'))
|
|
66
|
+
return resolve(fail('URL Facebook tidak valid'))
|
|
67
|
+
|
|
68
|
+
const res = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Linux; Android 13)', 'Accept-Language': 'id-ID,id;q=0.9' }, timeout: 20000 })
|
|
69
|
+
const $ = cheerio.load(res.data)
|
|
70
|
+
|
|
71
|
+
const title = ($('meta[property="og:description"]').attr('content') ||
|
|
72
|
+
$('meta[name="description"]').attr('content') ||
|
|
73
|
+
$('meta[property="og:title"]').attr('content') || 'Facebook Photo')
|
|
74
|
+
.replace(/\| Facebook.*$/i, '').replace(/&/g, '&').replace(/\s+/g, ' ').trim()
|
|
75
|
+
|
|
76
|
+
const medias = []
|
|
77
|
+
const og = $('meta[property="og:image"]').attr('content')
|
|
78
|
+
if (og) medias.push(og)
|
|
79
|
+
|
|
80
|
+
const matches = res.data.match(/https:\/\/scontent.*?\.(jpg|png|jpeg).*?(?=")/g) || []
|
|
81
|
+
for (let img of matches) {
|
|
82
|
+
img = img.replace(/\\\//g, '/').replace(/\\u0025/g, '%')
|
|
83
|
+
medias.push(img)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const clean = [...new Set(medias)]
|
|
87
|
+
if (!clean.length) return resolve(fail('Foto tidak ditemukan'))
|
|
88
|
+
|
|
89
|
+
resolve(ok({ title, total: clean.length, medias: clean.map(v => ({ type: 'image', url: v })) }))
|
|
90
|
+
} catch (e) { console.log(e); resolve(fail(e?.response?.status || e.message)) }
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { facebook, facebookPhoto }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { axios, ok, fail } = require('../utils')
|
|
2
|
+
|
|
3
|
+
const _formatBytes = (bytes) => {
|
|
4
|
+
if (!bytes) return null
|
|
5
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
|
6
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
|
7
|
+
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Google Drive Downloader
|
|
12
|
+
* @param {string} url
|
|
13
|
+
*/
|
|
14
|
+
const gdrive = async (url) => {
|
|
15
|
+
return new Promise(async (resolve) => {
|
|
16
|
+
try {
|
|
17
|
+
if (!url || !url.includes('drive.google.com'))
|
|
18
|
+
return resolve(fail('URL Google Drive tidak valid'))
|
|
19
|
+
|
|
20
|
+
const id = url.match(/\/d\/(.*?)\//)?.[1] || url.match(/[?&]id=([^&]+)/)?.[1] || null
|
|
21
|
+
if (!id) return resolve(fail('File ID tidak ditemukan'))
|
|
22
|
+
|
|
23
|
+
const preview = `https://drive.google.com/file/d/${id}/view`
|
|
24
|
+
const direct = `https://drive.google.com/uc?export=download&id=${id}`
|
|
25
|
+
|
|
26
|
+
const page = await axios.get(preview, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 15000 })
|
|
27
|
+
const filename = page.data.match(/<title>(.*?) - Google Drive<\/title>/)?.[1] || 'unknown'
|
|
28
|
+
|
|
29
|
+
const head = await axios({ url: direct, method: 'GET', maxRedirects: 5, responseType: 'stream', headers: { 'User-Agent': 'Mozilla/5.0' } })
|
|
30
|
+
const filesize = _formatBytes(Number(head.headers['content-length']) || 0)
|
|
31
|
+
const mimetype = head.headers['content-type'] || 'unknown'
|
|
32
|
+
|
|
33
|
+
resolve(ok({ id, filename, filesize, mimetype, direct, preview }))
|
|
34
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { gdrive }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { cheerio, ok, fail } = require('../utils')
|
|
2
|
+
const vm = require('vm')
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Instagram Downloader — foto, video, reels, carousel
|
|
6
|
+
* Sumber: snapsave.app
|
|
7
|
+
* @param {string} url - URL post/reel Instagram
|
|
8
|
+
*/
|
|
9
|
+
const instagram = async (url) => {
|
|
10
|
+
return new Promise(async (resolve) => {
|
|
11
|
+
try {
|
|
12
|
+
if (!url || !url.includes('instagram.com'))
|
|
13
|
+
return resolve(fail('URL Instagram tidak valid'))
|
|
14
|
+
|
|
15
|
+
const axios = require('axios')
|
|
16
|
+
const res = await axios.post(
|
|
17
|
+
'https://snapsave.app/action.php?lang=id',
|
|
18
|
+
'url=' + encodeURIComponent(url),
|
|
19
|
+
{
|
|
20
|
+
headers: {
|
|
21
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.82 Mobile Safari/537.36',
|
|
22
|
+
'Referer': 'https://snapsave.app/id/download-video-instagram',
|
|
23
|
+
'Origin': 'https://snapsave.app',
|
|
24
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
25
|
+
},
|
|
26
|
+
timeout: 15000,
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// Decode obfuscated JS
|
|
31
|
+
let decoded = ''
|
|
32
|
+
const sandbox = {
|
|
33
|
+
decodeURIComponent, escape, unescape, String, Math,
|
|
34
|
+
window: { location: { hostname: 'snapsave.app' } },
|
|
35
|
+
document: { getElementById: () => ({ set innerHTML(v) { decoded = v } }) },
|
|
36
|
+
eval: (s) => { decoded = s; return s },
|
|
37
|
+
}
|
|
38
|
+
try { vm.runInNewContext(res.data, sandbox) } catch (_) {}
|
|
39
|
+
|
|
40
|
+
if (!decoded) return resolve(fail('Gagal decode response'))
|
|
41
|
+
|
|
42
|
+
// Unescape escaped quotes dari dalam JS string
|
|
43
|
+
decoded = decoded.replace(/\\"/g, '"').replace(/\\'/g, "'")
|
|
44
|
+
|
|
45
|
+
const $ = cheerio.load(decoded)
|
|
46
|
+
const medias = []
|
|
47
|
+
|
|
48
|
+
$('.download-items').each((_, el) => {
|
|
49
|
+
const thumb = $(el).find('img').attr('src') || null
|
|
50
|
+
const dlUrl = $(el).find('a[href*="rapidcdn"]').attr('href') || null
|
|
51
|
+
const isVideo = $(el).find('i[class*="dlvideo"]').length > 0
|
|
52
|
+
if (dlUrl) medias.push({ type: isVideo ? 'video' : 'image', url: dlUrl, thumbnail: thumb })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!medias.length) return resolve(fail('Media tidak ditemukan'))
|
|
56
|
+
|
|
57
|
+
resolve(ok({ url, medias }))
|
|
58
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { instagram }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { fetchHTML, cheerio, ok, fail } = require('../utils')
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* MediaFire Downloader
|
|
5
|
+
* @param {string} url - URL MediaFire
|
|
6
|
+
*/
|
|
7
|
+
const mediafire = async (url) => {
|
|
8
|
+
return new Promise(async (resolve) => {
|
|
9
|
+
try {
|
|
10
|
+
if (!url || !url.includes('mediafire.com'))
|
|
11
|
+
return resolve(fail('URL MediaFire tidak valid'))
|
|
12
|
+
|
|
13
|
+
const html = await fetchHTML(url, { Referer: 'https://www.mediafire.com/' })
|
|
14
|
+
const $ = cheerio.load(html)
|
|
15
|
+
|
|
16
|
+
const download = $('#downloadButton').attr('href')
|
|
17
|
+
if (!download) return resolve(fail('Link download tidak ditemukan'))
|
|
18
|
+
|
|
19
|
+
const filename = $('.dl-btn-label').attr('title') ||
|
|
20
|
+
$('meta[property="og:title"]').attr('content') ||
|
|
21
|
+
download.split('/').pop()
|
|
22
|
+
const filesize = $('#downloadButton').text().replace(/Download/i, '').replace(/\s+/g, ' ').trim()
|
|
23
|
+
const filetype = filename.split('.').pop() || 'unknown'
|
|
24
|
+
|
|
25
|
+
resolve(ok({ url, filename, filesize, filetype, download }))
|
|
26
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { mediafire }
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const { axios, fetchHTML, ok, fail } = require('../utils')
|
|
2
|
+
|
|
3
|
+
const HEADERS = {
|
|
4
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.82 Mobile Safari/537.36',
|
|
5
|
+
'Accept-Language': 'id-ID,id;q=0.9',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const _id = (url, type) => url.match(new RegExp(`spotify\\.com\\/${type}\\/([a-zA-Z0-9]+)`))?.[1] || null
|
|
9
|
+
|
|
10
|
+
const _state = (html) => {
|
|
11
|
+
try {
|
|
12
|
+
const m = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/)
|
|
13
|
+
if (m) return JSON.parse(m[1])
|
|
14
|
+
} catch (_) {}
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const _cover = (entity) => {
|
|
19
|
+
const imgs = entity?.visualIdentity?.image || entity?.coverArt?.sources || entity?.images || []
|
|
20
|
+
if (!imgs.length) return null
|
|
21
|
+
return imgs.sort((a, b) => (b.maxWidth || b.width || 0) - (a.maxWidth || a.width || 0))[0]?.url || null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const _duration = (ms) => {
|
|
25
|
+
if (!ms || isNaN(ms)) return null
|
|
26
|
+
const m = Math.floor(ms / 60000)
|
|
27
|
+
const s = Math.floor((ms % 60000) / 1000).toString().padStart(2, '0')
|
|
28
|
+
return `${m}:${s}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Ambil anonymous access token dari Spotify web player
|
|
32
|
+
const _getToken = async () => {
|
|
33
|
+
const res = await axios.get('https://open.spotify.com/get_access_token?reason=transport&productType=web_player', {
|
|
34
|
+
headers: { ...HEADERS, 'Referer': 'https://open.spotify.com/' },
|
|
35
|
+
timeout: 10000,
|
|
36
|
+
})
|
|
37
|
+
return res.data?.accessToken || null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Cari video YouTube pertama dari query
|
|
41
|
+
const _ytSearch = async (query) => {
|
|
42
|
+
const html = await fetchHTML(
|
|
43
|
+
`https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`,
|
|
44
|
+
{ 'User-Agent': HEADERS['User-Agent'], 'Accept-Language': 'en-US,en;q=0.9' }
|
|
45
|
+
)
|
|
46
|
+
const m = html.match(/var ytInitialData\s*=\s*({[\s\S]*?});<\/script>/)
|
|
47
|
+
if (!m) return null
|
|
48
|
+
try {
|
|
49
|
+
const data = JSON.parse(m[1])
|
|
50
|
+
const sections = data?.contents?.twoColumnSearchResultsRenderer
|
|
51
|
+
?.primaryContents?.sectionListRenderer?.contents || []
|
|
52
|
+
for (const sec of sections) {
|
|
53
|
+
for (const item of sec?.itemSectionRenderer?.contents || []) {
|
|
54
|
+
if (item?.videoRenderer?.videoId) return item.videoRenderer.videoId
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Download audio via cobalt → fallback yt1s
|
|
62
|
+
const _ytDownload = async (videoId) => {
|
|
63
|
+
const ytUrl = `https://www.youtube.com/watch?v=${videoId}`
|
|
64
|
+
// Cobalt
|
|
65
|
+
try {
|
|
66
|
+
const res = await axios.post('https://co.wuk.sh/api/json',
|
|
67
|
+
{ url: ytUrl, isAudioOnly: true, aFormat: 'mp3', filenamePattern: 'basic' },
|
|
68
|
+
{ headers: { ...HEADERS, 'Accept': 'application/json', 'Content-Type': 'application/json' }, timeout: 20000 }
|
|
69
|
+
)
|
|
70
|
+
if (res.data?.url) return { url: res.data.url, source: 'cobalt' }
|
|
71
|
+
} catch (_) {}
|
|
72
|
+
// yt1s fallback
|
|
73
|
+
try {
|
|
74
|
+
const page = await axios.post('https://yt1s.com/api/ajaxSearch',
|
|
75
|
+
new URLSearchParams({ q: ytUrl, vt: 'home' }).toString(),
|
|
76
|
+
{ headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://yt1s.com/' }, timeout: 15000 }
|
|
77
|
+
)
|
|
78
|
+
const kId = page.data?.kId
|
|
79
|
+
const bid = Object.keys(page.data?.links?.mp3 || {})[0]
|
|
80
|
+
if (kId && bid) {
|
|
81
|
+
const conv = await axios.post('https://yt1s.com/api/ajaxConvert',
|
|
82
|
+
new URLSearchParams({ vid: videoId, k: bid }).toString(),
|
|
83
|
+
{ headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://yt1s.com/' }, timeout: 20000 }
|
|
84
|
+
)
|
|
85
|
+
if (conv.data?.dlink) return { url: conv.data.dlink, source: 'yt1s' }
|
|
86
|
+
}
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* Spotify Search Track
|
|
93
|
+
* @param {string} query
|
|
94
|
+
* @param {number} limit
|
|
95
|
+
*/
|
|
96
|
+
const spotifySearch = async (query, limit = 10) => {
|
|
97
|
+
return new Promise(async (resolve) => {
|
|
98
|
+
try {
|
|
99
|
+
if (!query) return resolve(fail('Query tidak boleh kosong'))
|
|
100
|
+
|
|
101
|
+
const token = await _getToken()
|
|
102
|
+
if (!token) return resolve(fail('Gagal mendapatkan token Spotify'))
|
|
103
|
+
|
|
104
|
+
const res = await axios.get(`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=${limit}`, {
|
|
105
|
+
headers: { ...HEADERS, 'Authorization': `Bearer ${token}` },
|
|
106
|
+
timeout: 15000,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const items = res.data?.tracks?.items || []
|
|
110
|
+
if (!items.length) return resolve(fail('Tidak ada hasil'))
|
|
111
|
+
|
|
112
|
+
resolve(ok(items.map(t => ({
|
|
113
|
+
id: t.id,
|
|
114
|
+
title: t.name,
|
|
115
|
+
artist: t.artists?.map(a => a.name).join(', ') || null,
|
|
116
|
+
album: t.album?.name || null,
|
|
117
|
+
release: t.album?.release_date || null,
|
|
118
|
+
duration: _duration(t.duration_ms),
|
|
119
|
+
cover: t.album?.images?.[0]?.url || null,
|
|
120
|
+
preview: t.preview_url || null,
|
|
121
|
+
url: t.external_urls?.spotify || `https://open.spotify.com/track/${t.id}`,
|
|
122
|
+
}))))
|
|
123
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* Spotify Track Info + Preview URL (30 detik)
|
|
129
|
+
* @param {string} url - URL track Spotify
|
|
130
|
+
*/
|
|
131
|
+
const spotifyTrack = async (url) => {
|
|
132
|
+
return new Promise(async (resolve) => {
|
|
133
|
+
try {
|
|
134
|
+
if (!url || !url.includes('spotify.com'))
|
|
135
|
+
return resolve(fail('URL Spotify tidak valid'))
|
|
136
|
+
|
|
137
|
+
const id = _id(url, 'track')
|
|
138
|
+
if (!id) return resolve(fail('Track ID tidak ditemukan'))
|
|
139
|
+
|
|
140
|
+
const html = await fetchHTML(`https://open.spotify.com/embed/track/${id}`, HEADERS)
|
|
141
|
+
const entity = _state(html)?.props?.pageProps?.state?.data?.entity
|
|
142
|
+
if (!entity) return resolve(fail('Data track tidak ditemukan'))
|
|
143
|
+
|
|
144
|
+
resolve(ok({
|
|
145
|
+
id,
|
|
146
|
+
title: entity.name || entity.title || null,
|
|
147
|
+
artist: (entity.artists || []).map(a => a.name).join(', ') || null,
|
|
148
|
+
release: entity.releaseDate?.isoString?.slice(0, 10) || null,
|
|
149
|
+
duration: _duration(entity.duration),
|
|
150
|
+
cover: _cover(entity),
|
|
151
|
+
preview: entity.audioPreview?.url || null,
|
|
152
|
+
url: `https://open.spotify.com/track/${id}`,
|
|
153
|
+
}))
|
|
154
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/*
|
|
159
|
+
* Spotify Download — cari di YouTube lalu download audio
|
|
160
|
+
* @param {string} url - URL track Spotify
|
|
161
|
+
*/
|
|
162
|
+
const spotifyDownload = async (url) => {
|
|
163
|
+
return new Promise(async (resolve) => {
|
|
164
|
+
try {
|
|
165
|
+
const track = await spotifyTrack(url)
|
|
166
|
+
if (!track.status) return resolve(track)
|
|
167
|
+
|
|
168
|
+
const { title, artist } = track.data
|
|
169
|
+
const query = `${title} ${artist} audio`
|
|
170
|
+
|
|
171
|
+
const ytId = await _ytSearch(query)
|
|
172
|
+
if (!ytId) return resolve(fail('Video YouTube tidak ditemukan'))
|
|
173
|
+
|
|
174
|
+
const dl = await _ytDownload(ytId)
|
|
175
|
+
|
|
176
|
+
resolve(ok({
|
|
177
|
+
...track.data,
|
|
178
|
+
youtube_id: ytId,
|
|
179
|
+
youtube_url: `https://www.youtube.com/watch?v=${ytId}`,
|
|
180
|
+
download: dl?.url || null,
|
|
181
|
+
dl_source: dl?.source || null,
|
|
182
|
+
}))
|
|
183
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* Spotify Album Info + list track (ringan)
|
|
189
|
+
* @param {string} url - URL album Spotify
|
|
190
|
+
*/
|
|
191
|
+
const spotifyAlbum = async (url) => {
|
|
192
|
+
return new Promise(async (resolve) => {
|
|
193
|
+
try {
|
|
194
|
+
if (!url || !url.includes('spotify.com'))
|
|
195
|
+
return resolve(fail('URL Spotify tidak valid'))
|
|
196
|
+
|
|
197
|
+
const id = _id(url, 'album')
|
|
198
|
+
if (!id) return resolve(fail('Album ID tidak ditemukan'))
|
|
199
|
+
|
|
200
|
+
const html = await fetchHTML(`https://open.spotify.com/embed/album/${id}`, HEADERS)
|
|
201
|
+
const entity = _state(html)?.props?.pageProps?.state?.data?.entity
|
|
202
|
+
if (!entity) return resolve(fail('Data album tidak ditemukan'))
|
|
203
|
+
|
|
204
|
+
const tracks = (entity.tracks?.items || []).map(t => ({
|
|
205
|
+
id: t.track?.id || t.uid || null,
|
|
206
|
+
title: t.track?.name || t.name || null,
|
|
207
|
+
artist: (t.track?.artists || t.artists || []).map(a => a.name).join(', '),
|
|
208
|
+
duration: _duration(t.track?.duration?.totalMilliseconds || t.track?.duration || t.duration),
|
|
209
|
+
}))
|
|
210
|
+
|
|
211
|
+
resolve(ok({
|
|
212
|
+
id,
|
|
213
|
+
title: entity.name || null,
|
|
214
|
+
artist: (entity.artists || []).map(a => a.name).join(', ') || null,
|
|
215
|
+
release: entity.date?.isoString?.slice(0, 10) || entity.releaseDate?.isoString?.slice(0, 10) || null,
|
|
216
|
+
total_track: entity.tracks?.totalCount || tracks.length,
|
|
217
|
+
cover: _cover(entity),
|
|
218
|
+
tracks,
|
|
219
|
+
url: `https://open.spotify.com/album/${id}`,
|
|
220
|
+
}))
|
|
221
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* Spotify Playlist Info + list track (ringan)
|
|
227
|
+
* @param {string} url - URL playlist Spotify
|
|
228
|
+
*/
|
|
229
|
+
const spotifyPlaylist = async (url) => {
|
|
230
|
+
return new Promise(async (resolve) => {
|
|
231
|
+
try {
|
|
232
|
+
if (!url || !url.includes('spotify.com'))
|
|
233
|
+
return resolve(fail('URL Spotify tidak valid'))
|
|
234
|
+
|
|
235
|
+
const id = _id(url, 'playlist')
|
|
236
|
+
if (!id) return resolve(fail('Playlist ID tidak ditemukan'))
|
|
237
|
+
|
|
238
|
+
const html = await fetchHTML(`https://open.spotify.com/embed/playlist/${id}`, HEADERS)
|
|
239
|
+
const entity = _state(html)?.props?.pageProps?.state?.data?.entity
|
|
240
|
+
if (!entity) return resolve(fail('Data playlist tidak ditemukan'))
|
|
241
|
+
|
|
242
|
+
const tracks = (entity.trackList || entity.tracks?.items || []).map(t => ({
|
|
243
|
+
id: t.uid || t.track?.id || null,
|
|
244
|
+
title: t.track?.name || t.name || null,
|
|
245
|
+
artist: (t.track?.artists || t.artists || []).map(a => a.name).join(', '),
|
|
246
|
+
}))
|
|
247
|
+
|
|
248
|
+
resolve(ok({
|
|
249
|
+
id,
|
|
250
|
+
title: entity.name || null,
|
|
251
|
+
owner: entity.ownerV2?.data?.name || null,
|
|
252
|
+
description: entity.description || null,
|
|
253
|
+
total_track: entity.tracks?.totalCount || tracks.length,
|
|
254
|
+
cover: _cover(entity),
|
|
255
|
+
tracks,
|
|
256
|
+
url: `https://open.spotify.com/playlist/${id}`,
|
|
257
|
+
}))
|
|
258
|
+
} catch (e) { console.log(e); resolve(fail(e)) }
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = { spotifySearch, spotifyTrack, spotifyDownload, spotifyAlbum, spotifyPlaylist }
|