@zenaveline/scraper 1.1.0 â 1.2.1
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 +31 -9
- package/package.json +8 -2
- package/src/bypasstools.js +35 -0
- package/src/douyindl.js +69 -0
- package/src/igstalk.js +56 -64
- package/src/spotify.js +374 -0
- package/src/spotifycard.js +142 -0
- package/src/spotifydl.js +25 -0
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
## đ Fitur Unggulan
|
|
19
19
|
- **⥠Super Ringan & Cepat:** Dibuat menggunakan library andalan.
|
|
20
20
|
- **đĻ Support Dual Module:** Bisa pakai `import` (ESM) atau `require` (CommonJS).
|
|
21
|
-
- **đĄ Developer Friendly:** Sintaks yang sederhana dan
|
|
21
|
+
- **đĄ Developer Friendly:** Sintaks yang sederhana dan mudah dipahami.
|
|
22
22
|
- **đ Beragam Layanan:** Mulai dari Instagram, Threads, Twitter (X), Pinterest, hingga Apple Music & AI dan lain lain.
|
|
23
23
|
|
|
24
24
|
## đĻ Instalasi
|
|
@@ -33,27 +33,44 @@ yarn add @zenaveline/scraper
|
|
|
33
33
|
|
|
34
34
|
## đ Quick Start
|
|
35
35
|
|
|
36
|
-
Cara panggilnya sangat praktis!
|
|
36
|
+
Cara panggilnya sangat praktis! Kamu bebas menamai *import*-nya sesuai selera (contoh: `scraper` atau nama lain).
|
|
37
37
|
|
|
38
38
|
### đ ESM (ECMAScript Modules)
|
|
39
39
|
Untuk project berformat module (atau menggunakan TypeScript):
|
|
40
40
|
|
|
41
41
|
```javascript
|
|
42
|
-
import
|
|
42
|
+
import scraper from '@zenaveline/scraper'
|
|
43
43
|
|
|
44
|
-
const data = await igdl('https://www.instagram.com/p/url-postingan')
|
|
45
|
-
console.log(data)
|
|
44
|
+
const data = await scraper.igdl('https://www.instagram.com/p/url-postingan')
|
|
45
|
+
console.log(data) // output
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
### đģ CommonJS (CJS)
|
|
49
49
|
Untuk project standar Node.js (seperti bot WhatsApp pada umumnya):
|
|
50
50
|
|
|
51
51
|
```javascript
|
|
52
|
-
const
|
|
52
|
+
const scraper = require('@zenaveline/scraper')
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const data = await scraper.igstalk('prabowo')
|
|
55
|
+
console.log(data); // output
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### đļ Khusus Penggunaan Spotify API
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
const scraper = require('@zenaveline/scraper')
|
|
62
|
+
|
|
63
|
+
const spotify = new scraper.spotify()
|
|
64
|
+
|
|
65
|
+
// Contoh pencarian lagu
|
|
66
|
+
const searchResult = await spotify.search('a thousand years jvke')
|
|
67
|
+
console.log(searchResult)
|
|
68
|
+
|
|
69
|
+
// Contoh Mengambil data spesifik berdasarkan ID
|
|
70
|
+
const trackData = await spotify.track('23WxsmliKISYaCfam8iPPG')
|
|
71
|
+
const artistData = await spotify.artist('164Uj4eKjl6zTBKfJLFKKK')
|
|
72
|
+
const albumData = await spotify.album('7z2Nuz41dkU9ruY5Qkva2L')
|
|
73
|
+
const playlistData = await spotify.playlist('4IfUIdWEonPys9mYs7zXna')
|
|
57
74
|
```
|
|
58
75
|
|
|
59
76
|
---
|
|
@@ -70,6 +87,7 @@ Panggil nama fungsinya sesuai daftar di bawah ini untuk menikmati semua fiturnya
|
|
|
70
87
|
| `threadsdownload(url)` | Ambil media foto/video dari postingan Threads secara instan. |
|
|
71
88
|
| `twetterdownload(url)` | Unduh postingan video/gambar dari platform X (Twitter). |
|
|
72
89
|
| `pinterestdownload(url)`| Ekstrak media gambar dan video dengan kualitas jernih dari Pinterest. |
|
|
90
|
+
| `douyindl(url)` | Download video Douyin tanpa watermark. |
|
|
73
91
|
|
|
74
92
|
### đĩ Musik & Audio
|
|
75
93
|
| Fungsi | Deskripsi Singkat |
|
|
@@ -77,6 +95,9 @@ Panggil nama fungsinya sesuai daftar di bawah ini untuk menikmati semua fiturnya
|
|
|
77
95
|
| `applemusicdl(url)` | Unduh audio lagu favoritmu langsung dari Apple Music (hingga 320kbps). |
|
|
78
96
|
| `geniussearch(query)` | Temukan lagu dan metadata sang artis lewat API Genius. |
|
|
79
97
|
| `geniusdetail(id)` | Dapatkan detail lagu beserta lirik utuhnya via Genius ID. |
|
|
98
|
+
| `spotify` | Class API untuk fitur Spotify (search, track, artist, album, playlist). Gunakan dengan `new scraper.spotify()`. |
|
|
99
|
+
| `spotifydl(url)` | Unduh track dari Spotify (mendukung URL Track). |
|
|
100
|
+
| `spotifycard(options)` | Buat gambar kartu keren lagu Spotify menggunakan metadata musik. |
|
|
80
101
|
|
|
81
102
|
### đ¤ AI & Image Tools
|
|
82
103
|
| Fungsi | Deskripsi Singkat |
|
|
@@ -88,6 +109,7 @@ Panggil nama fungsinya sesuai daftar di bawah ini untuk menikmati semua fiturnya
|
|
|
88
109
|
### âšī¸ Utility
|
|
89
110
|
| Fungsi | Deskripsi Singkat |
|
|
90
111
|
| :--- | :--- |
|
|
112
|
+
| `bypasstools(url)` | Bypass shortlink URL (support sf.gl, sub2unlock, tinyurl, linkvertise, dll). |
|
|
91
113
|
| `docs()` | Gunakan fungsi ini untuk melihat *list* lengkap seluruh fungsi scraper yang didukung secara terprogram. |
|
|
92
114
|
|
|
93
115
|
> **Catatan Penting:**
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenaveline/scraper",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Kumpulan scraper yang mudah digunakan!",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"@zenaveline/scraper",
|
|
7
|
+
"scraper",
|
|
8
|
+
"scraping"
|
|
9
|
+
],
|
|
5
10
|
"main": "index.js",
|
|
6
11
|
"type": "commonjs",
|
|
7
12
|
"exports": {
|
|
@@ -18,6 +23,7 @@
|
|
|
18
23
|
"dependencies": {
|
|
19
24
|
"axios": "^1.6.0",
|
|
20
25
|
"cheerio": "^1.0.0",
|
|
21
|
-
"crypto": "^1.0.1"
|
|
26
|
+
"crypto": "^1.0.1",
|
|
27
|
+
"sharp": "^0.33.2"
|
|
22
28
|
}
|
|
23
29
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//credit by nazir
|
|
2
|
+
//support sf.gl, sub2unlock, tinyurl, linkvertise dll
|
|
3
|
+
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
|
|
6
|
+
async function bypass(url, androidId = crypto.randomBytes(16).toString("hex")) {
|
|
7
|
+
const deviceId = crypto.createHash("sha256").update(`bypasstools:${androidId}`).digest("hex");
|
|
8
|
+
const { sessionToken } = await fetch("https://bypass.tools/api/mobile/init", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
deviceId,
|
|
13
|
+
platform: "android",
|
|
14
|
+
appVersion: "1.0.0"
|
|
15
|
+
})
|
|
16
|
+
}).then(r => r.json());
|
|
17
|
+
return fetch("https://bypass.tools/api/mobile/bypass", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Authorization: `Bearer ${sessionToken}`,
|
|
22
|
+
"X-Device-ID": deviceId
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
url,
|
|
26
|
+
forceRefresh: false
|
|
27
|
+
})
|
|
28
|
+
}).then(async r => {
|
|
29
|
+
const d = await r.json();
|
|
30
|
+
if (!r.ok) throw new Error(d.message || "Bypass failed");
|
|
31
|
+
return d.result;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = bypass;
|
package/src/douyindl.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//Š Nazir
|
|
2
|
+
|
|
3
|
+
async function getAwemeId(input) {
|
|
4
|
+
if (/^\d+$/.test(input)) return input;
|
|
5
|
+
if (input.includes('v.douyin.com')) {
|
|
6
|
+
const res = await fetch(input, { redirect: 'follow' });
|
|
7
|
+
input = res.url;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const match = input.match(/\/video\/(\d+)/);
|
|
11
|
+
if (match) return match[1];
|
|
12
|
+
|
|
13
|
+
throw new Error("Gagal mengekstrak ID dari URL.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function douyin(input) {
|
|
17
|
+
try {
|
|
18
|
+
const awemeId = await getAwemeId(input);
|
|
19
|
+
const url = "https://so.douyin.com/aweme/v1/web/aweme/detail/";
|
|
20
|
+
|
|
21
|
+
const headers = {
|
|
22
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
23
|
+
"accept": "application/json",
|
|
24
|
+
"user-agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36"
|
|
25
|
+
};
|
|
26
|
+
const body = `aweme_id=${awemeId}&request_source=280`;
|
|
27
|
+
|
|
28
|
+
const response = await fetch(url, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: headers,
|
|
31
|
+
body: body
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
35
|
+
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
|
|
38
|
+
if (!data.aweme_detail) {
|
|
39
|
+
throw new Error("Metadata tidak ditemukan.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const videoData = data.aweme_detail.video;
|
|
43
|
+
const desc = data.aweme_detail.desc || "no_description";
|
|
44
|
+
const author = data.aweme_detail.author ? data.aweme_detail.author.nickname : "unknown";
|
|
45
|
+
const videoUrl = videoData.play_addr.url_list[0];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
status: "success",
|
|
49
|
+
video_id: awemeId,
|
|
50
|
+
metadata: {
|
|
51
|
+
description: desc,
|
|
52
|
+
author: author,
|
|
53
|
+
author_id: data.aweme_detail.author ? data.aweme_detail.author.unique_id : null,
|
|
54
|
+
statistics: data.aweme_detail.statistics,
|
|
55
|
+
cover: videoData.cover.url_list[0],
|
|
56
|
+
duration: Math.round(videoData.duration / 1000) + "s",
|
|
57
|
+
video_url: videoUrl
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return {
|
|
63
|
+
status: "error",
|
|
64
|
+
message: error.message
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = douyin;
|
package/src/igstalk.js
CHANGED
|
@@ -1,67 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
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
|
-
name,
|
|
57
|
-
username: parsedUsername,
|
|
58
|
-
followers,
|
|
59
|
-
following,
|
|
60
|
-
posts,
|
|
61
|
-
bio,
|
|
62
|
-
profilePic: image,
|
|
63
|
-
url: url || `https://www.instagram.com/${username}`
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
async function igStalk(username) {
|
|
5
|
+
const [ky, tx] = ['2180a186d666f77b2c2af70e0098166d2b43d1e0a7b199776b07b98bf8351f37', 1781788719883]
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const client = axios.create({
|
|
9
|
+
baseURL: 'https://cors.caliph.my.id/https://api-wh.storiesig.info/api/v1/instagram',
|
|
10
|
+
headers: {
|
|
11
|
+
'accept': 'application/json, text/plain, */*',
|
|
12
|
+
'accept-language': 'id,id-ID;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
13
|
+
'origin': 'https://storiesig.info',
|
|
14
|
+
'referer': 'https://storiesig.info/',
|
|
15
|
+
'user-agent': 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
|
|
16
|
+
'sec-ch-ua': '"Chromium";v="149", "Google Chrome";v="149", "Not/A)Brand";v="99"',
|
|
17
|
+
'sec-ch-ua-mobile': '?0',
|
|
18
|
+
'sec-ch-ua-platform': '"Linux"',
|
|
19
|
+
'sec-fetch-dest': 'empty',
|
|
20
|
+
'sec-fetch-mode': 'cors',
|
|
21
|
+
'sec-fetch-site': 'same-site'
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
client.interceptors.request.use((config) => {
|
|
26
|
+
const bd = config.data ?? {};
|
|
27
|
+
const ts = Date.now();
|
|
28
|
+
const mg = JSON.stringify(Object.keys(bd).sort().reduce((a, k) => { a[k] = bd[k]; return a; }, {})) + ts;
|
|
29
|
+
const sg = crypto.createHmac('sha256', Buffer.from(ky, 'hex')).update(mg).digest('hex');
|
|
30
|
+
config.data = { ...bd, ts, _ts: tx, _tsc: 0, _sv: 2, _s: sg };
|
|
31
|
+
return config;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { data: usrinf } = await client.post('userInfo', { username });
|
|
35
|
+
|
|
36
|
+
if ((!usrinf.sucess && usrinf?.result <= 0) || !usrinf.result?.find(p => p?.user?.username === username)) {
|
|
37
|
+
return {
|
|
38
|
+
status: false,
|
|
39
|
+
msg: 'User not found'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [{ data: stories }, { data: posts }] = await Promise.all([
|
|
44
|
+
client.post('stories', { username }),
|
|
45
|
+
client.post('postsV2', { username, maxId: '' })
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
status: true,
|
|
50
|
+
...usrinf.result[0].user,
|
|
51
|
+
stories: stories.result,
|
|
52
|
+
posts: posts.result
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw error
|
|
64
56
|
}
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
module.exports =
|
|
59
|
+
module.exports = igStalk;
|
package/src/spotify.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
|
|
4
|
+
class Parser {
|
|
5
|
+
_getImg(o) {
|
|
6
|
+
return (o?.sources || []).map(s => ({
|
|
7
|
+
url: s.url,
|
|
8
|
+
width: s.width || s.maxWidth || null,
|
|
9
|
+
height: s.height || s.maxHeight || null
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_getCol(o) {
|
|
14
|
+
return o?.extractedColors?.colorRaw?.hex || o?.extractedColors?.colorDark?.hex || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_getVI(v) {
|
|
18
|
+
return v?.squareCoverImage?.extractedColorSet ? {
|
|
19
|
+
text_color: v.squareCoverImage.extractedColorSet.encoreBaseSetTextColor || null,
|
|
20
|
+
high_contrast: v.squareCoverImage.extractedColorSet.highContrast || null,
|
|
21
|
+
higher_contrast: v.squareCoverImage.extractedColorSet.higherContrast || null,
|
|
22
|
+
min_contrast: v.squareCoverImage.extractedColorSet.minContrast || null
|
|
23
|
+
} : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_getLink(uri) {
|
|
27
|
+
if (!uri) return { id: null, url: null };
|
|
28
|
+
const p = uri.split(':');
|
|
29
|
+
return {
|
|
30
|
+
uri,
|
|
31
|
+
id: p[2] || null,
|
|
32
|
+
url: p[2] ? `https://open.spotify.com/${p[1]}/${p[2]}` : null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
parseSearch(res) {
|
|
37
|
+
if (!res) return null;
|
|
38
|
+
|
|
39
|
+
const parse = (arr, mapFn, isTrack = false) => (arr || []).reduce((acc, node) => {
|
|
40
|
+
const d = isTrack ? node.item?.data : node.data;
|
|
41
|
+
if (d) acc.push({ ...mapFn(d), ...(node.matchedFields && { matched_fields: node.matchedFields }) });
|
|
42
|
+
return acc;
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const trackItems = res.tracksV2?.items?.length ? res.tracksV2.items : res.topResultsV2?.itemsV2?.filter(i => i.item?.__typename === "TrackResponseWrapper");
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
top_results: (res.topResultsV2?.itemsV2 || []).reduce((acc, node) => {
|
|
49
|
+
const wrap = node.item;
|
|
50
|
+
const d = wrap?.data;
|
|
51
|
+
if (!d) return acc;
|
|
52
|
+
const type = wrap.__typename?.replace('ResponseWrapper', '') || 'Unknown';
|
|
53
|
+
acc.push({
|
|
54
|
+
type: type, ...this._getLink(d.uri),
|
|
55
|
+
name: d.name || d.profile?.name || d.displayName || null,
|
|
56
|
+
images: this._getImg(d.coverArt || d.visuals?.avatarImage || d.images?.items?.[0] || d.avatar),
|
|
57
|
+
matched_fields: node.matchedFields || []
|
|
58
|
+
});
|
|
59
|
+
return acc;
|
|
60
|
+
}, []),
|
|
61
|
+
tracks: parse(trackItems, t => ({
|
|
62
|
+
...this._getLink(t.uri), name: t.name || null, duration_ms: t.duration?.totalMilliseconds || 0,
|
|
63
|
+
explicit: t.contentRating?.label === "EXPLICIT", media_type: t.trackMediaType || null,
|
|
64
|
+
playability: { playable: !!t.playability?.playable, reason: t.playability?.reason || null },
|
|
65
|
+
associations: { audio_count: t.associationsV3?.audioAssociations?.totalCount || 0, video_count: t.associationsV3?.videoAssociations?.totalCount || 0 },
|
|
66
|
+
artists: (t.artists?.items || []).map(a => ({ ...this._getLink(a.uri), uri: a.uri, name: a.profile?.name })),
|
|
67
|
+
album: {
|
|
68
|
+
...this._getLink(t.albumOfTrack?.uri), name: t.albumOfTrack?.name || null,
|
|
69
|
+
images: this._getImg(t.albumOfTrack?.coverArt), color_dark: this._getCol(t.albumOfTrack?.coverArt), visual_identity: this._getVI(t.albumOfTrack?.visualIdentity)
|
|
70
|
+
},
|
|
71
|
+
sixteen_by_nine_cover: t.visualIdentity?.sixteenByNineCoverImage?.image?.data?.sources || []
|
|
72
|
+
}), true),
|
|
73
|
+
albums: parse(res.albumsV2?.items, a => ({
|
|
74
|
+
...this._getLink(a.uri), name: a.name || null, type: a.type || null, release_year: a.date?.year || null,
|
|
75
|
+
playability: { playable: !!a.playability?.playable, reason: a.playability?.reason || null },
|
|
76
|
+
artists: (a.artists?.items || []).map(art => ({ ...this._getLink(art.uri), uri: art.uri, name: art.profile?.name })),
|
|
77
|
+
images: this._getImg(a.coverArt), color_dark: this._getCol(a.coverArt), visual_identity: this._getVI(a.visualIdentity)
|
|
78
|
+
})),
|
|
79
|
+
artists: parse(res.artists?.items, art => ({
|
|
80
|
+
...this._getLink(art.uri), name: art.profile?.name || null, images: this._getImg(art.visuals?.avatarImage), color_dark: this._getCol(art.visuals?.avatarImage), visual_identity: this._getVI(art.visualIdentity)
|
|
81
|
+
})),
|
|
82
|
+
episodes: parse(res.episodes?.items, ep => ({
|
|
83
|
+
...this._getLink(ep.uri), name: ep.name || null, description: ep.description || null, duration_ms: ep.duration?.totalMilliseconds || 0, explicit: ep.contentRating?.label === "EXPLICIT", media_types: ep.mediaTypes || [], release_date: ep.releaseDate?.isoString || null,
|
|
84
|
+
playability: { playable: ep.playability?.reason === "PLAYABLE", reason: ep.playability?.reason || null }, played_state: ep.playedState?.state || null, is_paywall: !!ep.restrictions?.paywallContent,
|
|
85
|
+
images: this._getImg(ep.coverArt), color_dark: this._getCol(ep.coverArt), visual_identity: this._getVI(ep.visualIdentity), video_preview_thumbnail: this._getImg(ep.videoPreviewThumbnail?.imagePreview?.data),
|
|
86
|
+
podcast: { ...this._getLink(ep.podcastV2?.data?.uri), name: ep.podcastV2?.data?.name || null, publisher: ep.podcastV2?.data?.publisher?.name || null, media_type: ep.podcastV2?.data?.mediaType || null }
|
|
87
|
+
})),
|
|
88
|
+
podcasts: parse(res.podcasts?.items, pod => ({
|
|
89
|
+
...this._getLink(pod.uri), name: pod.name || null, publisher: pod.publisher?.name || null, media_type: pod.mediaType || null,
|
|
90
|
+
topics: (pod.topics?.items || []).map(t => ({ ...this._getLink(t.uri), uri: t.uri, title: t.title })),
|
|
91
|
+
images: this._getImg(pod.coverArt), color_dark: this._getCol(pod.coverArt), visual_identity: this._getVI(pod.visualIdentity)
|
|
92
|
+
})),
|
|
93
|
+
playlists: parse(res.playlists?.items, pl => ({
|
|
94
|
+
...this._getLink(pl.uri), name: pl.name || null, description: pl.description || null, format: pl.format || null, attributes: pl.attributes || [],
|
|
95
|
+
images: this._getImg(pl.images?.items?.[0]), color_dark: this._getCol(pl.images?.items?.[0]), visual_identity: this._getVI(pl.visualIdentity),
|
|
96
|
+
owner: { ...this._getLink(pl.ownerV2?.data?.uri), display_name: pl.ownerV2?.data?.name || null, username: pl.ownerV2?.data?.username || null, images: this._getImg(pl.ownerV2?.data?.avatar) }
|
|
97
|
+
})),
|
|
98
|
+
genres: parse(res.genres?.items, g => ({
|
|
99
|
+
...this._getLink(g.uri), name: g.name || null, images: this._getImg(g.image), color_dark: this._getCol(g.image)
|
|
100
|
+
})),
|
|
101
|
+
users: parse(res.users?.items, u => ({
|
|
102
|
+
...this._getLink(u.uri), display_name: u.displayName || null, username: u.username || null, images: this._getImg(u.avatar), color_dark: this._getCol(u.avatar)
|
|
103
|
+
}))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parseTrack(data) {
|
|
108
|
+
const t = data?.track || data;
|
|
109
|
+
if (!t || t.__typename !== 'Track') return null;
|
|
110
|
+
const allArtists = [...(t.firstArtist?.items || []), ...(t.otherArtists?.items || [])];
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...this._getLink(t.uri), name: t.name || null, duration_ms: t.duration?.totalMilliseconds || 0,
|
|
114
|
+
playcount: parseInt(t.playcount) || 0, explicit: t.contentRating?.label === "EXPLICIT", track_number: t.trackNumber || null,
|
|
115
|
+
album: {
|
|
116
|
+
...this._getLink(t.albumOfTrack?.uri), name: t.albumOfTrack?.name || null, type: t.albumOfTrack?.type || null, release_year: t.albumOfTrack?.date?.year || null,
|
|
117
|
+
images: this._getImg(t.albumOfTrack?.coverArt), color: this._getCol(t.albumOfTrack?.coverArt), visual_identity: this._getVI(t.albumOfTrack?.visualIdentity)
|
|
118
|
+
},
|
|
119
|
+
artists: allArtists.map(node => ({ ...this._getLink(node.uri), name: node.profile?.name || null, images: this._getImg(node.visuals?.avatarImage) }))
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
parseArtist(data) {
|
|
124
|
+
const a = data?.artist || data;
|
|
125
|
+
if (!a || a.__typename !== 'Artist') return null;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...this._getLink(a.uri || `spotify:artist:${a.id}`), uri: a.uri || `spotify:artist:${a.id}`, name: a.profile?.name || null, verified: !!a.profile?.verified,
|
|
129
|
+
images: this._getImg(a.visuals?.avatarImage), header_images: this._getImg(a.visuals?.headerImage?.data || a.headerImage?.data), color: this._getCol(a.visuals?.avatarImage),
|
|
130
|
+
statistics: { followers: a.stats?.followers || 0, monthly_listeners: a.stats?.monthlyListeners || 0 },
|
|
131
|
+
top_tracks: (a.discography?.topTracks?.items || []).map(node => ({
|
|
132
|
+
...this._getLink(node.track?.uri), name: node.track?.name || null, playcount: parseInt(node.track?.playcount) || 0, duration_ms: node.track?.duration?.totalMilliseconds || 0,
|
|
133
|
+
album: { ...this._getLink(node.track?.albumOfTrack?.uri), name: node.track?.albumOfTrack?.name || null, images: this._getImg(node.track?.albumOfTrack?.coverArt) }
|
|
134
|
+
}))
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
parseAlbum(data) {
|
|
139
|
+
const al = data?.albumUnion || data?.album || data;
|
|
140
|
+
if (!al || (al.__typename !== 'Album' && al.__typename !== 'AlbumRelease')) return null;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...this._getLink(al.uri), name: al.name || null, type: al.type || null,
|
|
144
|
+
release_date: al.date?.isoString || al.date?.year || null, label: al.label || null,
|
|
145
|
+
playability: { playable: !!al.playability?.playable, reason: al.playability?.reason || null },
|
|
146
|
+
images: this._getImg(al.coverArt), color: this._getCol(al.coverArt), visual_identity: this._getVI(al.visualIdentity),
|
|
147
|
+
artists: (al.artists?.items || []).map(art => ({ ...this._getLink(art.uri), name: art.profile?.name || null })),
|
|
148
|
+
copyrights: al.copyrights?.items || [],
|
|
149
|
+
tracks: (al.tracks?.items || al.tracksV2?.items || []).map(node => {
|
|
150
|
+
const t = node.track || node;
|
|
151
|
+
return {
|
|
152
|
+
...this._getLink(t.uri), name: t.name || null, duration_ms: t.duration?.totalMilliseconds || 0,
|
|
153
|
+
playcount: parseInt(t.playcount) || 0, explicit: t.contentRating?.label === "EXPLICIT", track_number: t.trackNumber || null,
|
|
154
|
+
artists: (t.artists?.items || []).map(a => ({ ...this._getLink(a.uri), uri: a.uri, name: a.profile?.name }))
|
|
155
|
+
};
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parsePlaylist(data) {
|
|
161
|
+
const pl = data?.playlistV2 || data?.playlist || data;
|
|
162
|
+
if (!pl || (pl.__typename !== 'Playlist' && pl.__typename !== 'PlaylistResponseWrapper')) return null;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...this._getLink(pl.uri), name: pl.name || null, description: pl.description || null, format: pl.format || null,
|
|
166
|
+
followers: pl.followers || pl.ownerV2?.data?.followers || 0,
|
|
167
|
+
images: this._getImg(pl.images?.items?.[0] || pl.image), color: this._getCol(pl.images?.items?.[0] || pl.image), visual_identity: this._getVI(pl.visualIdentity),
|
|
168
|
+
owner: {
|
|
169
|
+
...this._getLink(pl.ownerV2?.data?.uri), display_name: pl.ownerV2?.data?.name || null, username: pl.ownerV2?.data?.username || null,
|
|
170
|
+
images: this._getImg(pl.ownerV2?.data?.avatar)
|
|
171
|
+
},
|
|
172
|
+
tracks: (pl.content?.items || pl.tracks?.items || []).map(node => {
|
|
173
|
+
const t = node.item?.data || node.track || node;
|
|
174
|
+
if (!t || t.__typename !== 'Track') return null;
|
|
175
|
+
return {
|
|
176
|
+
...this._getLink(t.uri), name: t.name || null, duration_ms: t.duration?.totalMilliseconds || 0, explicit: t.contentRating?.label === "EXPLICIT",
|
|
177
|
+
album: { ...this._getLink(t.albumOfTrack?.uri), name: t.albumOfTrack?.name || null, images: this._getImg(t.albumOfTrack?.coverArt) },
|
|
178
|
+
artists: (t.artists?.items || []).map(a => ({ ...this._getLink(a.uri), uri: a.uri, name: a.profile?.name }))
|
|
179
|
+
};
|
|
180
|
+
}).filter(item => item !== null)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Spotify {
|
|
187
|
+
constructor() {
|
|
188
|
+
this.cfg = {
|
|
189
|
+
secret: '376136387538459893883312310911992847112448894410210511297108',
|
|
190
|
+
version: 61,
|
|
191
|
+
client_version: '1.2.88.61.ge172202b',
|
|
192
|
+
query: {
|
|
193
|
+
search: {
|
|
194
|
+
opt: "searchDesktop",
|
|
195
|
+
sha: "21b3fe49546912ba782db5c47e9ef5a7dbd20329520ba0c7d0fcfadee671d24e"
|
|
196
|
+
},
|
|
197
|
+
track: {
|
|
198
|
+
opt: "getTrack",
|
|
199
|
+
sha: "612585ae06ba435ad26369870deaae23b5c8800a256cd8a57e08eddc25a37294",
|
|
200
|
+
},
|
|
201
|
+
artist: {
|
|
202
|
+
opt: "queryArtistOverview",
|
|
203
|
+
sha: "5b9e64f43843fa3a9b6a98543600299b0a2cbbbccfdcdcef2402eb9c1017ca4c"
|
|
204
|
+
},
|
|
205
|
+
album: {
|
|
206
|
+
opt: "getAlbum",
|
|
207
|
+
sha: "b9bfabef66ed756e5e13f68a942deb60bd4125ec1f1be8cc42769dc0259b4b10"
|
|
208
|
+
},
|
|
209
|
+
playlist: {
|
|
210
|
+
opt: "fetchPlaylist",
|
|
211
|
+
sha: "32b05e92e438438408674f95d0fdad8082865dc32acd55bd97f5113b8579092b"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
this.is = axios.create({
|
|
216
|
+
headers: {
|
|
217
|
+
'referer': 'https://open.spotify.com/',
|
|
218
|
+
'origin': 'https://open.spotify.com',
|
|
219
|
+
'content-type': 'application/json',
|
|
220
|
+
'accept': 'application/json',
|
|
221
|
+
'user-agent': 'Mozilla/5.0 (Linux; Android 16; NX729J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.7499.34 Mobile Safari/537.36',
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
this.parser = new Parser();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
generateTOTP(tsms) {
|
|
228
|
+
const counter = Math.floor((tsms / 1000) / 30);
|
|
229
|
+
const buffer = Buffer.alloc(8);
|
|
230
|
+
buffer.writeBigInt64BE(BigInt(counter));
|
|
231
|
+
const hmac = crypto.createHmac('sha1', Buffer.from(this.cfg.secret, "utf8")).update(buffer);
|
|
232
|
+
const digest = hmac.digest();
|
|
233
|
+
const offset = digest[digest.length - 1] & 0xf;
|
|
234
|
+
const code = (digest.readUInt32BE(offset) & 0x7fffffff) % 1000000;
|
|
235
|
+
return code.toString().padStart(6, '0');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async getToken() {
|
|
239
|
+
try {
|
|
240
|
+
if (this.is.defaults.headers.authorization) return true
|
|
241
|
+
const sts = Math.floor(Date.now() / 1000);
|
|
242
|
+
const { data: token } = await this.is.get("https://open.spotify.com/api/token", {
|
|
243
|
+
params: {
|
|
244
|
+
reason: "init",
|
|
245
|
+
productType: "web-player",
|
|
246
|
+
totp: this.generateTOTP(Date.now()),
|
|
247
|
+
totpServer: this.generateTOTP(sts * 1000),
|
|
248
|
+
totpVer: String(this.cfg.version)
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const { data: client } = await this.is.post('https://clienttoken.spotify.com/v1/clienttoken', {
|
|
252
|
+
client_data: {
|
|
253
|
+
client_version: this.cfg.client_version,
|
|
254
|
+
client_id: token.clientId,
|
|
255
|
+
js_sdk_data: {
|
|
256
|
+
device_brand: "unknown",
|
|
257
|
+
device_model: "unknown",
|
|
258
|
+
os: "linux",
|
|
259
|
+
os_version: "24.04",
|
|
260
|
+
device_id: crypto.randomUUID(),
|
|
261
|
+
device_type: "computer"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
Object.assign(this.is.defaults.headers, {
|
|
267
|
+
'accept-language': 'en',
|
|
268
|
+
'app-platform': 'WebPlayer',
|
|
269
|
+
'authorization': `Bearer ${token.accessToken}`,
|
|
270
|
+
'client-token': client.granted_token.token,
|
|
271
|
+
'spotify-app-version': this.cfg.client_version
|
|
272
|
+
})
|
|
273
|
+
return true
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async query(name, vars) {
|
|
280
|
+
try {
|
|
281
|
+
if (!(await this.getToken())) return;
|
|
282
|
+
const sel = this.cfg.query[name]
|
|
283
|
+
|
|
284
|
+
const { data: res } = await this.is.post('https://api-partner.spotify.com/pathfinder/v2/query', {
|
|
285
|
+
variables: vars,
|
|
286
|
+
operationName: sel.opt,
|
|
287
|
+
extensions: {
|
|
288
|
+
persistedQuery: {
|
|
289
|
+
version: 1,
|
|
290
|
+
sha256Hash: sel.sha
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return res
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw error
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async search(query) {
|
|
302
|
+
try {
|
|
303
|
+
const res = await this.query("search", {
|
|
304
|
+
searchTerm: query,
|
|
305
|
+
offset: 0,
|
|
306
|
+
limit: 10,
|
|
307
|
+
numberOfTopResults: 5,
|
|
308
|
+
includeAudiobooks: true,
|
|
309
|
+
includeArtistHasConcertsField: false,
|
|
310
|
+
includePreReleases: true,
|
|
311
|
+
includeAuthors: false,
|
|
312
|
+
includeEpisodeContentRatingsV2: false
|
|
313
|
+
});
|
|
314
|
+
return this.parser.parseSearch(res.data.searchV2)
|
|
315
|
+
} catch (error) {
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async track(ids) {
|
|
321
|
+
try {
|
|
322
|
+
const res = await this.query("track", {
|
|
323
|
+
uri: `spotify:track:${ids}`
|
|
324
|
+
});
|
|
325
|
+
return this.parser.parseTrack(res.data.trackUnion)
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw error
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async artist(ids) {
|
|
332
|
+
try {
|
|
333
|
+
const res = await this.query("artist", {
|
|
334
|
+
uri: `spotify:artist:${ids}`,
|
|
335
|
+
locale: "",
|
|
336
|
+
preReleaseV2: false
|
|
337
|
+
});
|
|
338
|
+
return this.parser.parseArtist(res.data.artistUnion)
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw error
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async album(ids) {
|
|
345
|
+
try {
|
|
346
|
+
const res = await this.query("album", {
|
|
347
|
+
uri: `spotify:album:${ids}`,
|
|
348
|
+
locale: "",
|
|
349
|
+
offset: 0,
|
|
350
|
+
limit: 50
|
|
351
|
+
});
|
|
352
|
+
return this.parser.parseAlbum(res.data.albumUnion)
|
|
353
|
+
} catch (error) {
|
|
354
|
+
throw error
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async playlist(ids) {
|
|
359
|
+
try {
|
|
360
|
+
const res = await this.query("playlist", {
|
|
361
|
+
uri: `spotify:playlist:${ids}`,
|
|
362
|
+
offset: 0,
|
|
363
|
+
limit: 25,
|
|
364
|
+
enableWatchFeedEntrypoint: false,
|
|
365
|
+
includeEpisodeContentRatingsV2: false
|
|
366
|
+
});
|
|
367
|
+
return this.parser.parsePlaylist(res.data.playlistV2)
|
|
368
|
+
} catch (error) {
|
|
369
|
+
throw error
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
module.exports = Spotify;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const sharp = require('sharp')
|
|
2
|
+
const axios = require('axios')
|
|
3
|
+
|
|
4
|
+
async function loadImageFromURL(url) {
|
|
5
|
+
if (!url) return null;
|
|
6
|
+
if (Buffer.isBuffer(url)) return url;
|
|
7
|
+
try {
|
|
8
|
+
const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 4000 });
|
|
9
|
+
return Buffer.from(response.data, 'binary');
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function truncateText(text, maxChars = 20) {
|
|
16
|
+
if (text.length <= maxChars) return text;
|
|
17
|
+
return text.slice(0, maxChars - 3).trim() + '...';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function wrapText(text, maxCharsPerLine = 22) {
|
|
21
|
+
if (!text) return [];
|
|
22
|
+
const words = text.split(' ');
|
|
23
|
+
const lines = [];
|
|
24
|
+
let currentLine = '';
|
|
25
|
+
|
|
26
|
+
for (const word of words) {
|
|
27
|
+
if ((currentLine + word).length > maxCharsPerLine) {
|
|
28
|
+
if (currentLine) lines.push(currentLine.trim());
|
|
29
|
+
currentLine = word + ' ';
|
|
30
|
+
} else {
|
|
31
|
+
currentLine += word + ' ';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (currentLine) lines.push(currentLine.trim());
|
|
35
|
+
return lines;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const escapeXml = (unsafe) => (unsafe || "").replace(/[<>&'"]/g, c => {
|
|
39
|
+
switch (c) {
|
|
40
|
+
case '<': return '<';
|
|
41
|
+
case '>': return '>';
|
|
42
|
+
case '&': return '&';
|
|
43
|
+
case '\'': return ''';
|
|
44
|
+
case '"': return '"';
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async function drawCardSpotify({ bg, cover, title, artist }) {
|
|
50
|
+
const width = 320;
|
|
51
|
+
const height = 420;
|
|
52
|
+
const cardX = 20;
|
|
53
|
+
const cardY = 20;
|
|
54
|
+
const cardWidth = 280;
|
|
55
|
+
const cardHeight = 380;
|
|
56
|
+
const radius = 20;
|
|
57
|
+
|
|
58
|
+
let baseImageBuffer;
|
|
59
|
+
let bgColor = '#222222';
|
|
60
|
+
|
|
61
|
+
const coverBuffer = await loadImageFromURL(cover);
|
|
62
|
+
let dominantColor = bgColor;
|
|
63
|
+
if (coverBuffer) {
|
|
64
|
+
try {
|
|
65
|
+
const stats = await sharp(coverBuffer).stats();
|
|
66
|
+
const { r, g, b } = stats.dominant;
|
|
67
|
+
dominantColor = `rgb(${r}, ${g}, ${b})`;
|
|
68
|
+
} catch (e) { }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (bg) {
|
|
72
|
+
const bgBuffer = await loadImageFromURL(bg);
|
|
73
|
+
if (bgBuffer) {
|
|
74
|
+
baseImageBuffer = await sharp(bgBuffer).resize(width, height, { fit: 'cover' }).toBuffer();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!baseImageBuffer && coverBuffer) {
|
|
79
|
+
baseImageBuffer = await sharp({ create: { width, height, channels: 4, background: dominantColor } }).png().toBuffer();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!baseImageBuffer) {
|
|
83
|
+
baseImageBuffer = await sharp({ create: { width, height, channels: 4, background: bgColor } }).png().toBuffer();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const composites = [];
|
|
87
|
+
|
|
88
|
+
const cardSvg = `<svg width="${width}" height="${height}">
|
|
89
|
+
<rect x="${cardX}" y="${cardY}" width="${cardWidth}" height="${cardHeight}" rx="${radius}" ry="${radius}" fill="rgba(0, 0, 0, 0.7)" />
|
|
90
|
+
</svg>`;
|
|
91
|
+
composites.push({ input: Buffer.from(cardSvg), top: 0, left: 0 });
|
|
92
|
+
|
|
93
|
+
if (coverBuffer) {
|
|
94
|
+
const resizedCover = await sharp(coverBuffer).resize(240, 240, { fit: 'cover' }).toBuffer();
|
|
95
|
+
composites.push({
|
|
96
|
+
input: resizedCover,
|
|
97
|
+
left: cardX + 20,
|
|
98
|
+
top: cardY + 20
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let titleLines = wrapText(truncateText(title || "", 26), 20);
|
|
103
|
+
let artistLines = wrapText(truncateText(artist || ""), 28);
|
|
104
|
+
|
|
105
|
+
let currentY = cardY + 282;
|
|
106
|
+
let textSvgStr = `<svg width="${width}" height="${height}">
|
|
107
|
+
<style>
|
|
108
|
+
.t { font-family: sans-serif; font-weight: bold; font-size: 22px; fill: white; }
|
|
109
|
+
.a { font-family: sans-serif; font-size: 16px; fill: rgba(255, 255, 255, 0.8); }
|
|
110
|
+
</style>
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
for (const line of titleLines) {
|
|
114
|
+
textSvgStr += `<text x="${cardX + 20}" y="${currentY}" class="t">${escapeXml(line)}</text>`;
|
|
115
|
+
currentY += 26;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
currentY += 2;
|
|
119
|
+
|
|
120
|
+
for (const line of artistLines) {
|
|
121
|
+
textSvgStr += `<text x="${cardX + 20}" y="${currentY}" class="a">${escapeXml(line)}</text>`;
|
|
122
|
+
currentY += 20;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
textSvgStr += `<text x="${cardX + 40}" y="${cardY + 370}" font-family="sans-serif" font-weight="bold" font-size="14px" fill="white">Spotify</text>
|
|
126
|
+
</svg>`;
|
|
127
|
+
|
|
128
|
+
composites.push({ input: Buffer.from(textSvgStr), top: 0, left: 0 });
|
|
129
|
+
|
|
130
|
+
const logoUrl = "https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_White-300x300.png";
|
|
131
|
+
const logoBuffer = await loadImageFromURL(logoUrl);
|
|
132
|
+
if (logoBuffer) {
|
|
133
|
+
const logoResized = await sharp(logoBuffer).resize(20, 20).toBuffer();
|
|
134
|
+
composites.push({ input: logoResized, top: cardY + 354, left: cardX + 14 });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const finalBuffer = await sharp(baseImageBuffer).composite(composites).png().toBuffer();
|
|
138
|
+
|
|
139
|
+
return finalBuffer;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = drawCardSpotify;
|
package/src/spotifydl.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
|
|
3
|
+
async function SpotifyDl(url) {
|
|
4
|
+
try {
|
|
5
|
+
const { data: pp } = await axios.post('https://gamepvz.com/api/download/get-url', {
|
|
6
|
+
url: url
|
|
7
|
+
}, {
|
|
8
|
+
'headers': {
|
|
9
|
+
'content-type': 'application/json',
|
|
10
|
+
'user-agent': 'Mozilla/5.0 (Linux; Android 16; NX729J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.7499.34 Mobile Safari/537.36',
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
return {
|
|
14
|
+
status: true,
|
|
15
|
+
title: pp.title,
|
|
16
|
+
author: pp.authorName,
|
|
17
|
+
cover: pp.coverUrl,
|
|
18
|
+
dl: atob(pp.originalVideoUrl.split('url=')[1])
|
|
19
|
+
}
|
|
20
|
+
} catch(e) {
|
|
21
|
+
return { status: false }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = SpotifyDl;
|