@danonino/moon 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 +1 -0
- package/index.js +25 -0
- package/index.mjs +16 -0
- package/package.json +33 -0
- package/src/allinonedownloader.js +66 -0
- package/src/applemusicdl.js +63 -0
- package/src/brat.js +192 -0
- package/src/bypasstools.js +35 -0
- package/src/douyindl.js +69 -0
- package/src/fake-ovo.js +106 -0
- package/src/geniusdetail.js +44 -0
- package/src/geniussearch.js +42 -0
- package/src/igdl.js +53 -0
- package/src/igstalk.js +59 -0
- package/src/iqc-darkmode.js +382 -0
- package/src/iqc-pinkmode.js +390 -0
- package/src/mediafiredl.js +144 -0
- package/src/nanobanana.js +115 -0
- package/src/novaai.js +34 -0
- package/src/pinterestdownload.js +72 -0
- package/src/pixaremovebg.js +32 -0
- package/src/savetube.js +85 -0
- package/src/sharpify.js +37 -0
- package/src/spotify.js +374 -0
- package/src/spotifycard.js +142 -0
- package/src/spotifydl.js +25 -0
- package/src/threadsdownload.js +65 -0
- package/src/tiktokdm-qc.js +225 -0
- package/src/tiktokdownload.js +56 -0
- package/src/twetterdownload.js +39 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
|
|
3
|
+
async function loadImageFromURL(url) {
|
|
4
|
+
if (!url) return null;
|
|
5
|
+
if (Buffer.isBuffer(url)) return url;
|
|
6
|
+
try {
|
|
7
|
+
const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 4000 });
|
|
8
|
+
return Buffer.from(response.data, 'binary');
|
|
9
|
+
} catch (err) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function truncateText(text, maxChars = 20) {
|
|
15
|
+
if (text.length <= maxChars) return text;
|
|
16
|
+
return text.slice(0, maxChars - 3).trim() + '...';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function wrapText(text, maxCharsPerLine = 22) {
|
|
20
|
+
if (!text) return [];
|
|
21
|
+
const words = text.split(' ');
|
|
22
|
+
const lines = [];
|
|
23
|
+
let currentLine = '';
|
|
24
|
+
|
|
25
|
+
for (const word of words) {
|
|
26
|
+
if ((currentLine + word).length > maxCharsPerLine) {
|
|
27
|
+
if (currentLine) lines.push(currentLine.trim());
|
|
28
|
+
currentLine = word + ' ';
|
|
29
|
+
} else {
|
|
30
|
+
currentLine += word + ' ';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (currentLine) lines.push(currentLine.trim());
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const escapeXml = (unsafe) => (unsafe || "").replace(/[<>&'"]/g, c => {
|
|
38
|
+
switch (c) {
|
|
39
|
+
case '<': return '<';
|
|
40
|
+
case '>': return '>';
|
|
41
|
+
case '&': return '&';
|
|
42
|
+
case '\'': return ''';
|
|
43
|
+
case '"': return '"';
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async function drawCardSpotify({ bg, cover, title, artist }) {
|
|
49
|
+
const sharp = require('sharp')
|
|
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;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
const cheerio = require('cheerio')
|
|
3
|
+
|
|
4
|
+
async function threadsdl(url) {
|
|
5
|
+
const get = await axios.get('https://sssthreads.net/')
|
|
6
|
+
const cookies = get.headers['set-cookie'].map(v => v.split(';')[0]).join('; ')
|
|
7
|
+
const $get = cheerio.load(get.data)
|
|
8
|
+
const csrf = $get('meta[name="csrf-token"]').attr('content')
|
|
9
|
+
|
|
10
|
+
const res = await axios.post('https://sssthreads.net/fetch-data',{ url },
|
|
11
|
+
{
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'X-CSRF-TOKEN': csrf,
|
|
15
|
+
origin: 'https://sssthreads.net',
|
|
16
|
+
referer: 'https://sssthreads.net/',
|
|
17
|
+
Cookie: cookies
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const $ = cheerio.load(res.data.html)
|
|
23
|
+
|
|
24
|
+
const author = {
|
|
25
|
+
username: $('.author-name').text().trim() || null,
|
|
26
|
+
avatar: $('.author-avatar').attr('src') || null,
|
|
27
|
+
caption: $('.post-description').text().trim() || null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const media = []
|
|
31
|
+
|
|
32
|
+
$('.media-item').each((_, el) => {
|
|
33
|
+
const thumb = $(el).find('.thumbnail-img').attr('data-src') || null
|
|
34
|
+
|
|
35
|
+
const links = $(el).find('.download-link')
|
|
36
|
+
let video = null
|
|
37
|
+
let mp3 = null
|
|
38
|
+
let image = null
|
|
39
|
+
|
|
40
|
+
links.each((__, a) => {
|
|
41
|
+
const href = $(a).attr('href')
|
|
42
|
+
const text = $(a).text().toLowerCase()
|
|
43
|
+
|
|
44
|
+
if (text.includes('video')) video = href
|
|
45
|
+
else if (text.includes('mp3')) mp3 = href
|
|
46
|
+
else if (text.includes('photo')) image = href
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
if (video || image) {
|
|
50
|
+
media.push({
|
|
51
|
+
type: video ? 'video' : 'image',
|
|
52
|
+
thumbnail: thumb,
|
|
53
|
+
download: video || image,
|
|
54
|
+
mp3: mp3
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
author,
|
|
61
|
+
media
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = threadsdl
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Source : https://whatsapp.com/channel/0029VbCHRSDAzNboLatr0W0o/471
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { createCanvas, loadImage, GlobalFonts } = require('@napi-rs/canvas');
|
|
6
|
+
const { writeFile, mkdir } = require('node:fs/promises');
|
|
7
|
+
const { existsSync } = require('node:fs');
|
|
8
|
+
const { join } = require('node:path');
|
|
9
|
+
const https = require('node:https');
|
|
10
|
+
const http = require('node:http');
|
|
11
|
+
|
|
12
|
+
const ASSETS_DIR = join(__dirname, 'assets');
|
|
13
|
+
const FONTS_DIR = join(ASSETS_DIR, 'fonts');
|
|
14
|
+
const TEMPLATE_PATH = join(ASSETS_DIR, 'template.png');
|
|
15
|
+
const TEMPLATE_URL = 'https://raw.githubusercontent.com/Ditzzx-vibecoder/Assets/main/ttqc/qyzwa.png';
|
|
16
|
+
|
|
17
|
+
const FONT_ASSETS = [
|
|
18
|
+
{ name: 'PlusJakartaSans-Regular', file: 'PlusJakartaSans-Regular.ttf', url: 'https://raw.githubusercontent.com/Ditzzx-vibecoder/Assets/main/ttqc/PlusJakartaSans-Regular.ttf', family: 'Plus Jakarta Sans' },
|
|
19
|
+
{ name: 'PlusJakartaSans-Medium', file: 'PlusJakartaSans-Medium.ttf', url: 'https://raw.githubusercontent.com/Ditzzx-vibecoder/Assets/main/ttqc/PlusJakartaSans-Medium.ttf', family: 'Plus Jakarta Sans' },
|
|
20
|
+
{ name: 'PlusJakartaSans-Bold', file: 'PlusJakartaSans-Bold.ttf', url: 'https://raw.githubusercontent.com/Ditzzx-vibecoder/Assets/main/ttqc/PlusJakartaSans-Bold.ttf', family: 'Plus Jakarta Sans' },
|
|
21
|
+
{ name: 'FontAwesome-Solid', file: 'fa-solid-900.ttf', url: 'https://raw.githubusercontent.com/Ditzzx-vibecoder/Assets/main/ttqc/fa-solid-900.ttf', family: 'Font Awesome 6 Free' },
|
|
22
|
+
{ name: 'NotoColorEmoji', file: 'NotoColorEmoji.ttf', url: 'https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf', family: 'Noto Color Emoji' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const MENU_ICONS = [
|
|
26
|
+
{ unicode: '\uf3e5', text: 'Balas', color: '#000000' },
|
|
27
|
+
{ unicode: '\uf064', text: 'Teruskan', color: '#000000' },
|
|
28
|
+
{ unicode: '\uf0c5', text: 'Salin', color: '#000000' },
|
|
29
|
+
{ unicode: '\uf1ab', text: 'Terjemahkan', color: '#000000' },
|
|
30
|
+
{ unicode: '\uf2ed', text: 'Hapus untuk saya', color: '#000000' },
|
|
31
|
+
{ unicode: '\uf024', text: 'Laporkan', color: '#ea4335' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const config = {
|
|
35
|
+
topPPX: 183, topPPY: 83, topPPRadius: 42,
|
|
36
|
+
topNameX: 250, topNameY: 82, topNameSize: 34,
|
|
37
|
+
chatPPX: 75, chatPPRadius: 38,
|
|
38
|
+
textX: 175, textY: 962,
|
|
39
|
+
bubbleWidth: 520, textSize: 30,
|
|
40
|
+
bubbleBgColor: '#ffffff', textColor: '#161823',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function fetchBuffer(url) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const client = url.startsWith('https') ? https : http;
|
|
46
|
+
client.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
|
|
47
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
48
|
+
return fetchBuffer(res.headers.location).then(resolve).catch(reject);
|
|
49
|
+
}
|
|
50
|
+
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode} → ${url}`));
|
|
51
|
+
const chunks = [];
|
|
52
|
+
res.on('data', c => chunks.push(c));
|
|
53
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
54
|
+
res.on('error', reject);
|
|
55
|
+
}).on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function ensureAssets() {
|
|
60
|
+
await mkdir(FONTS_DIR, { recursive: true });
|
|
61
|
+
|
|
62
|
+
if (!existsSync(TEMPLATE_PATH)) {
|
|
63
|
+
await writeFile(TEMPLATE_PATH, await fetchBuffer(TEMPLATE_URL));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const font of FONT_ASSETS) {
|
|
67
|
+
const dest = join(FONTS_DIR, font.file);
|
|
68
|
+
if (!existsSync(dest)) {
|
|
69
|
+
await writeFile(dest, await fetchBuffer(font.url));
|
|
70
|
+
}
|
|
71
|
+
GlobalFonts.registerFromPath(dest, font.family);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadImageSmart(src) {
|
|
76
|
+
if (src.startsWith('http://') || src.startsWith('https://')) {
|
|
77
|
+
return loadImage(await fetchBuffer(src));
|
|
78
|
+
}
|
|
79
|
+
return loadImage(src);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Wrap teks dengan support native font fallback (Jauh lebih bersih dan akurat)
|
|
83
|
+
function wrapText(ctx, text, maxWidth) {
|
|
84
|
+
const words = text.split(/(\s+)/);
|
|
85
|
+
const lines = [];
|
|
86
|
+
let currentLine = '';
|
|
87
|
+
|
|
88
|
+
for (const word of words) {
|
|
89
|
+
if (!word) continue;
|
|
90
|
+
if (word.trim() === '' && currentLine === '') continue;
|
|
91
|
+
|
|
92
|
+
const testLine = currentLine + word;
|
|
93
|
+
if (ctx.measureText(testLine).width > maxWidth) {
|
|
94
|
+
if (currentLine !== '') {
|
|
95
|
+
lines.push(currentLine.trimEnd());
|
|
96
|
+
currentLine = word.trimStart();
|
|
97
|
+
} else {
|
|
98
|
+
lines.push(testLine);
|
|
99
|
+
currentLine = '';
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
currentLine = testLine;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (currentLine.trim()) {
|
|
106
|
+
lines.push(currentLine.trimEnd());
|
|
107
|
+
}
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function drawRoundedRect(ctx, x, y, w, h, r, fill, stroke = null, shadow = false) {
|
|
112
|
+
ctx.save();
|
|
113
|
+
if (shadow) {
|
|
114
|
+
ctx.shadowColor = 'rgba(0,0,0,0.05)';
|
|
115
|
+
ctx.shadowBlur = 40;
|
|
116
|
+
ctx.shadowOffsetY = 12;
|
|
117
|
+
}
|
|
118
|
+
ctx.fillStyle = fill;
|
|
119
|
+
ctx.beginPath();
|
|
120
|
+
ctx.moveTo(x + r, y);
|
|
121
|
+
ctx.lineTo(x + w - r, y);
|
|
122
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
123
|
+
ctx.lineTo(x + w, y + h - r);
|
|
124
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
125
|
+
ctx.lineTo(x + r, y + h);
|
|
126
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
127
|
+
ctx.lineTo(x, y + r);
|
|
128
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
129
|
+
ctx.closePath();
|
|
130
|
+
ctx.fill();
|
|
131
|
+
if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = 1; ctx.stroke(); }
|
|
132
|
+
ctx.restore();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function drawCircleImage(ctx, img, cx, cy, r) {
|
|
136
|
+
ctx.save();
|
|
137
|
+
ctx.beginPath();
|
|
138
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
139
|
+
ctx.closePath();
|
|
140
|
+
ctx.clip();
|
|
141
|
+
ctx.drawImage(img, cx - r, cy - r, r * 2, r * 2);
|
|
142
|
+
ctx.restore();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function render(username, chatText, avatarSrc, outputPath) {
|
|
146
|
+
await ensureAssets();
|
|
147
|
+
|
|
148
|
+
const USERNAME = username ?? 'Ditzzx';
|
|
149
|
+
const CHAT_TEXT = chatText ?? 'Just friend kok cemburu 😂😂';
|
|
150
|
+
const AVATAR_SRC = avatarSrc ?? 'https://raw.githubusercontent.com/Ditzzx-vibecoder/Assets/6b71d84a580f385bd7ee36402df5341ead4770a0/Image/artworks-gWLRE6HyPH3DgVMG-ZFFxtg-t500x500.jpg';
|
|
151
|
+
const OUT = outputPath ?? join(__dirname, `tiktok-chat-${Date.now()}.png`);
|
|
152
|
+
|
|
153
|
+
const templateImage = await loadImage(TEMPLATE_PATH);
|
|
154
|
+
const avatarImage = await loadImageSmart(AVATAR_SRC);
|
|
155
|
+
|
|
156
|
+
const canvas = createCanvas(1080 * 2, 2280 * 2);
|
|
157
|
+
const ctx = canvas.getContext('2d');
|
|
158
|
+
|
|
159
|
+
ctx.scale(2, 2);
|
|
160
|
+
ctx.imageSmoothingEnabled = true;
|
|
161
|
+
ctx.imageSmoothingQuality = 'high';
|
|
162
|
+
|
|
163
|
+
ctx.clearRect(0, 0, 1080, 2280);
|
|
164
|
+
ctx.drawImage(templateImage, 0, 0, 1080, 2280);
|
|
165
|
+
|
|
166
|
+
drawCircleImage(ctx, avatarImage, config.topPPX, config.topPPY, config.topPPRadius);
|
|
167
|
+
|
|
168
|
+
ctx.font = `bold ${config.topNameSize}px 'Plus Jakarta Sans', 'Noto Color Emoji'`;
|
|
169
|
+
ctx.fillStyle = '#000000';
|
|
170
|
+
ctx.textAlign = 'left';
|
|
171
|
+
ctx.textBaseline = 'middle';
|
|
172
|
+
ctx.fillText(USERNAME, config.topNameX, config.topNameY);
|
|
173
|
+
|
|
174
|
+
ctx.font = `500 ${config.textSize}px 'Plus Jakarta Sans', 'Noto Color Emoji'`;
|
|
175
|
+
|
|
176
|
+
const lines = wrapText(ctx, CHAT_TEXT, config.bubbleWidth - 52);
|
|
177
|
+
const lineH = config.textSize * 1.45;
|
|
178
|
+
|
|
179
|
+
let maxW = 0;
|
|
180
|
+
for (const l of lines) {
|
|
181
|
+
const w = ctx.measureText(l).width;
|
|
182
|
+
if (w > maxW) maxW = w;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const padX = 30, padY = 24;
|
|
186
|
+
const bubbleW = Math.max(maxW + padX * 2, 180);
|
|
187
|
+
const bubbleH = lines.length * lineH + padY * 2;
|
|
188
|
+
const bubbleX = config.textX - padX;
|
|
189
|
+
const bubbleY = config.textY - padY;
|
|
190
|
+
|
|
191
|
+
drawCircleImage(ctx, avatarImage, config.chatPPX, bubbleY + bubbleH / 2, config.chatPPRadius);
|
|
192
|
+
drawRoundedRect(ctx, bubbleX, bubbleY, bubbleW, bubbleH, 35, config.bubbleBgColor);
|
|
193
|
+
|
|
194
|
+
ctx.fillStyle = config.textColor;
|
|
195
|
+
ctx.textAlign = 'left';
|
|
196
|
+
ctx.textBaseline = 'middle';
|
|
197
|
+
|
|
198
|
+
lines.forEach((line, i) => {
|
|
199
|
+
const lineY = config.textY + i * lineH + config.textSize / 2;
|
|
200
|
+
ctx.fillText(line, config.textX, lineY);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const menuX = 90, menuY = bubbleY + bubbleH + 28;
|
|
204
|
+
drawRoundedRect(ctx, menuX, menuY, 565, 580, 40, '#ffffff', 'rgba(0,0,0,0.02)', true);
|
|
205
|
+
|
|
206
|
+
const itemH = 90, iconX = menuX + 60, labelX = menuX + 130;
|
|
207
|
+
MENU_ICONS.forEach((item, i) => {
|
|
208
|
+
const cy = menuY + 25 + i * itemH + itemH / 2;
|
|
209
|
+
ctx.fillStyle = item.color;
|
|
210
|
+
ctx.font = `900 34px 'Font Awesome 6 Free'`;
|
|
211
|
+
ctx.textAlign = 'center';
|
|
212
|
+
ctx.textBaseline = 'middle';
|
|
213
|
+
ctx.fillText(item.unicode, iconX, cy);
|
|
214
|
+
ctx.font = `500 34px 'Plus Jakarta Sans'`;
|
|
215
|
+
ctx.textAlign = 'left';
|
|
216
|
+
ctx.fillText(item.text, labelX, cy);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
ctx.restore();
|
|
220
|
+
|
|
221
|
+
const pngData = await canvas.encode('png');
|
|
222
|
+
return pngData;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = render;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//credit: by Nazir
|
|
2
|
+
|
|
3
|
+
async function tt(url) {
|
|
4
|
+
const html = await fetch(url, {
|
|
5
|
+
headers: {
|
|
6
|
+
authority: "www.tiktok.com",
|
|
7
|
+
"sec-ch-ua-mobile": "?1",
|
|
8
|
+
"sec-ch-ua-platform": `"Android"`,
|
|
9
|
+
"user-agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36"
|
|
10
|
+
}
|
|
11
|
+
}).then(a => a.text());
|
|
12
|
+
const match = html.match(/<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>([\s\S]*?)<\/script>/);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
const json = JSON.parse(match[1]);
|
|
15
|
+
const data = json.__DEFAULT_SCOPE__["webapp.reflow.video.detail"].itemInfo.itemStruct;
|
|
16
|
+
let download = data.imagePost
|
|
17
|
+
? data.imagePost.images.reduce((acc, img) => {
|
|
18
|
+
return acc.concat(img.imageURL.urlList);
|
|
19
|
+
}, [])
|
|
20
|
+
: await fetch(`https://www.tiktok.com/player/api/v1/items?item_ids=${data.id}`)
|
|
21
|
+
.then(a => a.json())
|
|
22
|
+
.then(b => b.items[0].video_info.url_list[0]);
|
|
23
|
+
return {
|
|
24
|
+
id: data.id || data.aweme_id || null,
|
|
25
|
+
like: data.stats.diggCount || 0,
|
|
26
|
+
views: data.stats.playCount || data.play || 0,
|
|
27
|
+
share: data.stats.shareCount || 0,
|
|
28
|
+
comment: data.stats.commentCount || 0,
|
|
29
|
+
isVideo: data.imagePost ? false : true,
|
|
30
|
+
title: data.desc || data.suggestedWords?.[0] || "",
|
|
31
|
+
region: data.locationCreated || null,
|
|
32
|
+
duration: `${data.duration || data.music?.duration || 0} second`,
|
|
33
|
+
download: download,
|
|
34
|
+
author: {
|
|
35
|
+
id: data.author.id || "",
|
|
36
|
+
avatar: data.author?.avatarThumb,
|
|
37
|
+
nickname: data.author?.nickname || "",
|
|
38
|
+
username: data.author.uniqueId || "",
|
|
39
|
+
followers: data.author.followerCount || 0,
|
|
40
|
+
following: data.author.followingCount || 0,
|
|
41
|
+
like: data.author.heartCount || 0,
|
|
42
|
+
verified: data.author.verified,
|
|
43
|
+
videoCount: data.author.videoCount || 0
|
|
44
|
+
},
|
|
45
|
+
music: {
|
|
46
|
+
id: data.music?.id || null,
|
|
47
|
+
title: data.music?.title || "",
|
|
48
|
+
author: data.music?.authorName || "",
|
|
49
|
+
thumbnail: data.music?.coverLarge || data.music?.coverMedium || data.music?.coverThumb || null,
|
|
50
|
+
duration: data.music?.duration + " second" || "",
|
|
51
|
+
url: data.music?.playUrl || null
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = tt;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Base : https://ssstweet.com
|
|
3
|
+
Author : ZenzzXD
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const axios = require('axios')
|
|
7
|
+
|
|
8
|
+
const headers = {
|
|
9
|
+
'host': 'api-v1.menarailpost.com',
|
|
10
|
+
'sec-ch-ua': '"Chromium";v="139", "Not;A=Brand";v="99"',
|
|
11
|
+
'sec-ch-ua-platform': '"Android"',
|
|
12
|
+
'sec-ch-ua-mobile': '?1',
|
|
13
|
+
'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Mobile Safari/537.36',
|
|
14
|
+
'content-type': 'application/json',
|
|
15
|
+
'accept': '*/*',
|
|
16
|
+
'origin': 'https://ssstweet.com',
|
|
17
|
+
'sec-fetch-site': 'cross-site',
|
|
18
|
+
'sec-fetch-mode': 'cors',
|
|
19
|
+
'sec-fetch-dest': 'empty',
|
|
20
|
+
'referer': 'https://ssstweet.com/',
|
|
21
|
+
'accept-language': 'id-ID,id;q=0.9,en-AU;q=0.8,en;q=0.7,en-US;q=0.6'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function ssstweet(url) {
|
|
25
|
+
try {
|
|
26
|
+
const r = await axios.post('https://api-v1.menarailpost.com/v1/info',
|
|
27
|
+
{ url },
|
|
28
|
+
{ headers }
|
|
29
|
+
)
|
|
30
|
+
return r.data
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
status: false,
|
|
34
|
+
message: error.message
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = ssstweet
|