@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 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 to-the-point menggunakan Promise (Async/Await).
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! Gunakan _destructuring_ untuk mengimpor fungsi yang spesifik kamu butuhkan.
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 { igdl } from '@zenaveline/scraper';
42
+ import scraper from '@zenaveline/scraper'
43
43
 
44
- const data = await igdl('https://www.instagram.com/p/url-postingan');
45
- console.log(data); // JSON
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 { igdl } = require('@zenaveline/scraper');
52
+ const scraper = require('@zenaveline/scraper')
53
53
 
54
- // Pastikan dijalankan di dalam context fungsi async
55
- const data = await igdl('https://www.instagram.com/p/url-postingan');
56
- console.log(data); // JSON
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.0",
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;
@@ -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
- Base : https://instagram.com
3
- Author : ZenzzXD
4
- Test code nya di masing" ya jangan di website
5
- */
6
-
7
- const axios = require('axios')
8
- const cheerio = require('cheerio')
9
-
10
- const headers = {
11
- 'host': 'www.instagram.com',
12
- 'dpr': '1.962499976158142',
13
- 'viewport-width': '980',
14
- 'sec-ch-ua': '"Chromium";v="139", "Not;A=Brand";v="99"',
15
- 'sec-ch-ua-mobile': '?1',
16
- 'sec-ch-ua-platform': '"Android"',
17
- 'sec-ch-ua-platform-version': '"13.0.0"',
18
- 'sec-ch-ua-model': '"RMX3085"',
19
- 'sec-ch-ua-full-version-list': '"Chromium";v="139.0.7339.0", "Not;A=Brand";v="99.0.0.0"',
20
- 'sec-ch-prefers-color-scheme': 'dark',
21
- 'upgrade-insecure-requests': '1',
22
- 'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Mobile Safari/537.36',
23
- 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
24
- 'sec-fetch-site': 'same-origin',
25
- 'sec-fetch-mode': 'navigate',
26
- 'sec-fetch-user': '?1',
27
- 'sec-fetch-dest': 'document',
28
- 'accept-encoding': 'gzip, deflate, br',
29
- 'accept-language': 'id-ID,id;q=0.9,en-AU;q=0.8,en;q=0.7,en-US;q=0.6',
30
- 'cookie': 'csrftoken=UhNYfmpbN4zFIUATif4lcT; datr=aisGabstXUMaPhnsc3rGDr3V; ig_did=8961CBBD-2C6B-486E-97C0-61557817C61A; ps_l=1; ps_n=1; mid=aZVbfgABAAHGlIdJc5uThx4NQ4Qv; wd=551x1096; dpr=1.962499976158142',
31
- 'pragma': 'akamai-x-cache-on, akamai-x-cache-remote-on, akamai-x-check-cacheable, akamai-x-get-cache-key, akamai-x-get-extracted-values, status-id, akamai-x-get-true-cache-key, akamai-x-serial-no, akamai-x-get-request-id,akamai-x-get-nonces,akamai-x-get-client-ip,akamai-x-feo-trace'
32
- }
33
-
34
- async function instagram(username) {
35
- const r = await axios.get(`https://www.instagram.com/${username}`, { headers })
36
- const $ = cheerio.load(r.data)
37
-
38
- const title = $('meta[property="og:title"]').attr('content') || ''
39
- const description = $('meta[property="og:description"]').attr('content') || ''
40
- const image = $('meta[property="og:image"]').attr('content') || ''
41
- const url = $('meta[property="og:url"]').attr('content') || ''
42
- const nameDescription = $('meta[name="description"]').attr('content') || ''
43
-
44
- const followers = description.match(/([\d.,KMB]+)\s+(Pengikut|Followers)/i)?.[1] || '0'
45
- const following = description.match(/([\d.,KMB]+)\s+(Mengikuti|Following)/i)?.[1] || '0'
46
- const posts = description.match(/([\d.,KMB]+)\s+(Postingan|Posts)/i)?.[1] || '0'
47
-
48
- const nameUsernameMatch = title.match(/^([^(]+)\s+\(([^)]+)\)/)
49
- const name = nameUsernameMatch?.[1]?.trim() || ''
50
- const parsedUsername = nameUsernameMatch?.[2]?.replace('@', '').trim() || username
51
-
52
- const bioMatch = nameDescription.match(/(?:di Instagram|on Instagram):\s*["']?([^"']+)["']?$/i)
53
- const bio = bioMatch?.[1] || ''
54
-
55
- return {
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 = instagram
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 '&lt;';
41
+ case '>': return '&gt;';
42
+ case '&': return '&amp;';
43
+ case '\'': return '&apos;';
44
+ case '"': return '&quot;';
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;
@@ -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;