bianvigano commited on
Commit
d88fa4b
·
1 Parent(s): 035e5df

Refactor code structure and improve logging for better readability and error handling

Browse files
Files changed (1) hide show
  1. index.js +219 -157
index.js CHANGED
@@ -2,7 +2,7 @@ const os = require('os')
2
  const util = require('util')
3
  const axios = require('axios')
4
  const bytes = require('./lib/bytes')
5
- const sharp = require('sharp')
6
  const express = require('express')
7
  const cp = require('child_process')
8
  const PDFDocument = require('pdfkit')
@@ -17,38 +17,42 @@ const limitSize = '500mb'
17
  app.use(express.json({ limit: limitSize }))
18
  app.use(express.urlencoded({ extended: true, limit: limitSize }))
19
 
 
 
 
 
 
20
  const tmpFolder = os.tmpdir()
21
  app.use((req, res, next) => {
22
- fs.readdirSync(tmpFolder).map(file => {
23
- file = `${tmpFolder}/${file}`
24
- let stats = fs.statSync(file)
25
- if (!stats.isFile()) return
26
- if (Date.now() - stats.mtimeMs >= 1000 * 60 * 30) {
27
- fs.unlinkSync(file)
28
- console.log('Deleted file', file)
29
- }
30
- })
31
- next()
32
  })
33
 
34
  app.all('/', (req, res) => {
35
- const status = {}
36
- const used = process.memoryUsage()
37
- for (let key in used) status[key] = formatSize(used[key])
38
-
39
- const disk = cp.execSync('du -sh').toString().split('M')[0]
40
- status.diskUsage = `${disk} MB`
41
-
42
- const totalmem = os.totalmem()
43
- const freemem = os.freemem()
44
- status.memoryUsage = `${formatSize(totalmem - freemem)} / ${formatSize(totalmem)}`
45
-
46
- res.json({
47
- creator: '@Themeh',
48
- message: 'Hello World',
49
- uptime: new Date(process.uptime() * 1000).toUTCString().split(' ')[4],
50
- status
51
- })
52
  })
53
 
54
  app.get('/proxy', async (req, res) => {
@@ -78,125 +82,186 @@ app.get('/proxy', async (req, res) => {
78
  });
79
 
80
  app.get('/pages', async (req, res) => {
 
81
  const targetUrl = req.query.url;
82
 
83
  if (!targetUrl) {
 
84
  return res.status(400).json({ error: 'Parameter "url" dibutuhkan' });
85
  }
86
 
87
- const browser = await playwright.chromium.launch({
88
- headless: true,
89
- executablePath: '/usr/bin/chromium', // pastikan path ini benar di server kamu
90
- args: ['--no-sandbox']
91
- });
92
 
93
- const context = await browser.newContext({
94
- extraHTTPHeaders: {
95
- 'accept': '*/*',
96
- 'accept-language': 'en-US,en;q=0.9,id;q=0.8',
97
- 'cache-control': 'no-cache',
98
- 'origin': 'https://doujindesu.tv',
99
- 'pragma': 'no-cache',
100
- 'referer': 'https://doujindesu.tv',
101
- 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
102
- 'sec-ch-ua-mobile': '?0',
103
- 'sec-ch-ua-platform': '"Windows"',
104
- 'sec-fetch-dest': 'script',
105
- 'sec-fetch-mode': 'cors',
106
- 'sec-fetch-site': 'cross-site',
107
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
108
  }
109
- });
 
 
 
110
 
 
111
  try {
112
- const page = await context.newPage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
114
 
115
- // Tunggu ekstra jika perlu ngelewatin challenge
116
- await page.waitForTimeout(8000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
 
118
  const content = await page.content();
119
- res.send(content);
 
 
 
 
 
120
  } catch (err) {
121
- res.status(500).json({ error: 'Gagal fetch halaman', detail: err.message });
 
 
 
122
  } finally {
123
- await browser.close();
 
 
 
124
  }
125
  });
 
126
 
127
  app.all('/imagetopdf', async (req, res) => {
128
- if (!['POST'].includes(req.method)) return res.status(405).json({ success: false, message: 'Method Not Allowed' })
129
-
130
- try {
131
- console.log(new Date().toLocaleString('id', { timeZone: 'Asia/Jakarta' }), '\n', req.body)
132
- const { images } = req.body
133
- if (!(images && Array.isArray(images))) return res.json({ success: false, message: 'Required an array image url' })
134
-
135
- const buffer = await toPDF(images)
136
- res.setHeader('Content-Disposition', `attachment; filename=${Math.random().toString(36).slice(2)}.pdf`)
137
- res.setHeader('Content-Type', 'application/pdf')
138
- res.setHeader('Content-Length', buffer.byteLength)
139
- res.send(buffer)
140
- } catch (e) {
141
- console.log(e)
142
- e = String(e)
143
- res.status(500).json({ error: true, message: e === '[object Object]' ? 'Internal Server Error' : e })
144
- }
145
  })
146
 
147
  app.all('/webp2png', async (req, res) => {
148
- if (!['POST'].includes(req.method)) return res.status(405).json({ success: false, message: 'Method Not Allowed' })
149
-
150
- try {
151
- const { file } = req.body
152
- if (!(file && isBase64(file))) return res.json({ success: false, message: 'Payload body file must be filled in base64 format' })
153
-
154
- const fileBuffer = Buffer.from(file, 'base64')
155
- const fileName = `${Math.random().toString(36).slice(2)}.png`
156
- const convertData = await sharp(fileBuffer).png().toBuffer()
157
-
158
- await fs.promises.writeFile(`${tmpFolder}/${fileName}`, convertData)
159
- res.send(`https://${req.get('host')}/file/${fileName}`)
160
- } catch (e) {
161
- console.log(e)
162
- e = String(e)
163
- res.status(500).json({ error: true, message: e === '[object Object]' ? 'Internal Server Error' : e })
164
- }
165
  })
166
 
167
  app.all(['/webp2gif', '/webp2mp4'], async (req, res) => {
168
- if (!['POST'].includes(req.method)) return res.status(405).json({ success: false, message: 'Method Not Allowed' })
169
-
170
- try {
171
- const { file } = req.body
172
- if (!(file && isBase64(file))) return res.json({ success: false, message: 'Payload body file must be filled in base64 format' })
173
-
174
- const fileBuffer = Buffer.from(file, 'base64')
175
- const fileName = `${Math.random().toString(36).slice(2)}.webp`
176
- const filePath = `${tmpFolder}/${fileName}`
177
- await fs.promises.writeFile(filePath, fileBuffer)
178
-
179
- const exec = util.promisify(cp.exec).bind(cp)
180
- await exec(`convert ${filePath} ${filePath.replace('.webp', '.gif')}`)
181
- if (/gif/.test(req.path)) return res.send(`https://${req.get('host')}/file/${fileName.replace('.webp', '.gif')}`)
182
-
183
- await exec(`ffmpeg -i ${filePath.replace('.webp', '.gif')} -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ${filePath.replace(/.webp|.gif/g, '')}.mp4`)
184
- res.send(`https://${req.get('host')}/file/${fileName.replace('.webp', '.mp4')}`)
185
- } catch (e) {
186
- console.log(e)
187
- e = String(e)
188
- res.status(500).json({ error: true, message: e === '[object Object]' ? 'Internal Server Error' : e })
189
- }
190
  })
191
 
192
  app.get('/fetch', async (req, res) => {
193
- try {
194
- if (!req.query.url) return res.json({ message: 'Required an url' })
195
- let json = await axios.get(req.query.url)
196
- res.json(json.data)
197
- } catch (e) {
198
- res.send(e)
199
- }
200
  })
201
 
202
 
@@ -205,14 +270,13 @@ app.get('/fetch-page', async (req, res) => {
205
 
206
  if (!targetUrl) {
207
  return res.status(400).json({
208
- developer: "KyoukaDev",
209
  status: false,
210
  message: 'Parameter "url" dibutuhkan',
211
  });
212
  }
213
 
214
  const baseUrl = new URL(targetUrl).origin;
215
-
216
  const browser = await playwright.chromium.launch({
217
  headless: true,
218
  executablePath: '/usr/bin/chromium', // pastikan path ini valid
@@ -245,7 +309,6 @@ app.get('/fetch-page', async (req, res) => {
245
  const content = await page.content();
246
 
247
  res.json({
248
- developer: "KyoukaDev",
249
  status: true,
250
  data: {
251
  results: [
@@ -256,7 +319,6 @@ app.get('/fetch-page', async (req, res) => {
256
 
257
  } catch (err) {
258
  res.status(500).json({
259
- developer: "KyoukaDev",
260
  status: false,
261
  message: 'Gagal fetch halaman',
262
  error: err.message
@@ -270,45 +332,45 @@ const PORT = process.env.PORT || 7860
270
  app.listen(PORT, () => console.log('App running on port', PORT))
271
 
272
  function formatSize(num) {
273
- return bytes(+num || 0, { unitSeparator: ' ' })
274
  }
275
 
276
  function isBase64(str) {
277
- try {
278
- return btoa(atob(str)) === str
279
- } catch {
280
- return false
281
- }
282
  }
283
 
284
  function toPDF(urls) {
285
- return new Promise(async (resolve, reject) => {
286
- try {
287
- if (!Array.isArray(urls)) urls = [urls]
288
- const doc = new PDFDocument({ margin: 0, size: 'A4' })
289
- const buffers = []
290
-
291
- for (let i = 0; i < urls.length; i++) {
292
- const response = await fetch(urls[i], { headers: { referer: urls[i] }})
293
- if (!response.ok) continue
294
-
295
- const type = response.headers.get('content-type')
296
- if (!/image/.test(type)) continue
297
-
298
- let buffer = Buffer.from(await response.arrayBuffer())
299
- if (/gif|webp/.test(type)) buffer = await sharp(buffer).png().toBuffer()
300
-
301
- doc.image(buffer, 0, 0, { fit: [595.28, 841.89], align: 'center', valign: 'center' })
302
- if (urls.length !== i + 1) doc.addPage()
303
- }
304
-
305
- doc.on('data', (chunk) => buffers.push(chunk))
306
- doc.on('end', () => resolve(Buffer.concat(buffers)))
307
- doc.on('error', reject)
308
- doc.end()
309
- } catch (e) {
310
- console.log(e)
311
- reject(e)
312
- }
313
- })
314
  }
 
2
  const util = require('util')
3
  const axios = require('axios')
4
  const bytes = require('./lib/bytes')
5
+ const sharp = require('sharp')
6
  const express = require('express')
7
  const cp = require('child_process')
8
  const PDFDocument = require('pdfkit')
 
17
  app.use(express.json({ limit: limitSize }))
18
  app.use(express.urlencoded({ extended: true, limit: limitSize }))
19
 
20
+ // Logging helper
21
+ const log = (step, message) => {
22
+ console.log(`[${new Date().toISOString()}] [${step}] ${message}`);
23
+ };
24
+
25
  const tmpFolder = os.tmpdir()
26
  app.use((req, res, next) => {
27
+ fs.readdirSync(tmpFolder).map(file => {
28
+ file = `${tmpFolder}/${file}`
29
+ let stats = fs.statSync(file)
30
+ if (!stats.isFile()) return
31
+ if (Date.now() - stats.mtimeMs >= 1000 * 60 * 30) {
32
+ fs.unlinkSync(file)
33
+ console.log('Deleted file', file)
34
+ }
35
+ })
36
+ next()
37
  })
38
 
39
  app.all('/', (req, res) => {
40
+ const status = {}
41
+ const used = process.memoryUsage()
42
+ for (let key in used) status[key] = formatSize(used[key])
43
+
44
+ const disk = cp.execSync('du -sh').toString().split('M')[0]
45
+ status.diskUsage = `${disk} MB`
46
+
47
+ const totalmem = os.totalmem()
48
+ const freemem = os.freemem()
49
+ status.memoryUsage = `${formatSize(totalmem - freemem)} / ${formatSize(totalmem)}`
50
+
51
+ res.json({
52
+ message: 'Hello World',
53
+ uptime: new Date(process.uptime() * 1000).toUTCString().split(' ')[4],
54
+ status
55
+ })
 
56
  })
57
 
58
  app.get('/proxy', async (req, res) => {
 
82
  });
83
 
84
  app.get('/pages', async (req, res) => {
85
+ const startTime = Date.now();
86
  const targetUrl = req.query.url;
87
 
88
  if (!targetUrl) {
89
+ log('ERROR', 'Parameter "url" tidak disediakan');
90
  return res.status(400).json({ error: 'Parameter "url" dibutuhkan' });
91
  }
92
 
93
+ log('REQUEST', `Incoming request for: ${targetUrl}`);
 
 
 
 
94
 
95
+ // Validasi URL lebih awal (dan pastikan hanya http/https)
96
+ let parsedUrl;
97
+ try {
98
+ parsedUrl = new URL(targetUrl);
99
+ if (!/^https?:$/.test(parsedUrl.protocol)) {
100
+ log('ERROR', `Protocol tidak didukung: ${parsedUrl.protocol}`);
101
+ return res.status(400).json({ error: 'Hanya mendukung URL http/https' });
 
 
 
 
 
 
 
 
102
  }
103
+ } catch {
104
+ log('ERROR', `URL tidak valid: ${targetUrl}`);
105
+ return res.status(400).json({ error: 'URL tidak valid' });
106
+ }
107
 
108
+ let browser, context, page;
109
  try {
110
+ log('BROWSER', 'Meluncurkan Chromium...');
111
+ browser = await playwright.chromium.launch({
112
+ headless: true,
113
+ executablePath: '/usr/bin/chromium',
114
+ args: ['--no-sandbox']
115
+ });
116
+
117
+ context = await browser.newContext({
118
+ extraHTTPHeaders: {
119
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
120
+ 'accept-language': 'en-US,en;q=0.9,id;q=0.8',
121
+ 'cache-control': 'no-cache',
122
+ 'origin': parsedUrl.origin,
123
+ 'pragma': 'no-cache',
124
+ 'referer': parsedUrl.origin, // referer yang masuk akal untuk navigasi
125
+ 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
126
+ 'sec-ch-ua-mobile': '?0',
127
+ 'sec-ch-ua-platform': '"Windows"',
128
+ // Untuk navigasi dokumen, biarkan sec-fetch-* konsisten:
129
+ 'sec-fetch-dest': 'document',
130
+ 'sec-fetch-mode': 'navigate',
131
+ 'sec-fetch-site': 'none',
132
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
133
+ }
134
+ });
135
+
136
+ log('PAGE', 'Membuka halaman...');
137
+ page = await context.newPage();
138
+ page.setDefaultNavigationTimeout(60000);
139
+
140
  await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
141
 
142
+ // Coba tunggu sampai jaringan idle (kalau bisa)
143
+ try {
144
+ await page.waitForLoadState('networkidle', { timeout: 10000 });
145
+ } catch {
146
+ // abaikan kalau tidak idle-idle juga (beberapa situs stream request terus)
147
+ }
148
+
149
+ log('PAGE', 'Halaman dimuat. Mengecek Cloudflare challenge...');
150
+ const bodyText = (await page.textContent('body')) || '';
151
+
152
+ // Deteksi umum challenge Cloudflare
153
+ const maybeCF =
154
+ bodyText.includes('Checking your browser before accessing') ||
155
+ bodyText.includes('Just a moment') ||
156
+ (await page.title()).toLowerCase().includes('just a moment');
157
+
158
+ if (maybeCF) {
159
+ log('CHALLENGE', 'Cloudflare challenge terdeteksi. Menunggu 7 detik...');
160
+ await page.waitForTimeout(7000);
161
+ // Tambahan sedikit waktu untuk pemrosesan dinamis
162
+ log('WAIT', 'Tunggu tambahan 8 detik untuk pemrosesan dinamis...');
163
+ await page.waitForTimeout(8000);
164
+ } else {
165
+ // Meski tidak challenge, beri sedikit waktu untuk konten dinamis
166
+ log('WAIT', 'Tunggu singkat untuk konten dinamis...');
167
+ await page.waitForTimeout(1500);
168
+ }
169
 
170
+ log('EXTRACT', 'Mengambil konten halaman...');
171
  const content = await page.content();
172
+
173
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
174
+ log('DONE', `Berhasil fetch halaman dalam ${duration} detik`);
175
+
176
+ // Pastikan tipe respons
177
+ res.type('html').send(content);
178
  } catch (err) {
179
+ log('ERROR', `Gagal fetch: ${err.message}`);
180
+ if (!res.headersSent) {
181
+ res.status(500).json({ error: 'Gagal fetch halaman', detail: err.message });
182
+ }
183
  } finally {
184
+ log('CLEANUP', 'Menutup page/context/browser...');
185
+ try { if (page) await page.close({ runBeforeUnload: false }); } catch {}
186
+ try { if (context) await context.close(); } catch {}
187
+ try { if (browser) await browser.close(); } catch {}
188
  }
189
  });
190
+ app.use('/file', express.static(tmpFolder, { maxAge: 3600000 }))
191
 
192
  app.all('/imagetopdf', async (req, res) => {
193
+ if (!['POST'].includes(req.method)) return res.status(405).json({ success: false, message: 'Method Not Allowed' })
194
+
195
+ try {
196
+ console.log(new Date().toLocaleString('id', { timeZone: 'Asia/Jakarta' }), '\n', req.body)
197
+ const { images } = req.body
198
+ if (!(images && Array.isArray(images))) return res.json({ success: false, message: 'Required an array image url' })
199
+
200
+ const buffer = await toPDF(images)
201
+ res.setHeader('Content-Disposition', `attachment; filename=${Math.random().toString(36).slice(2)}.pdf`)
202
+ res.setHeader('Content-Type', 'application/pdf')
203
+ res.setHeader('Content-Length', buffer.byteLength)
204
+ res.send(buffer)
205
+ } catch (e) {
206
+ console.log(e)
207
+ e = String(e)
208
+ res.status(500).json({ error: true, message: e === '[object Object]' ? 'Internal Server Error' : e })
209
+ }
210
  })
211
 
212
  app.all('/webp2png', async (req, res) => {
213
+ if (!['POST'].includes(req.method)) return res.status(405).json({ success: false, message: 'Method Not Allowed' })
214
+
215
+ try {
216
+ const { file } = req.body
217
+ if (!(file && isBase64(file))) return res.json({ success: false, message: 'Payload body file must be filled in base64 format' })
218
+
219
+ const fileBuffer = Buffer.from(file, 'base64')
220
+ const fileName = `${Math.random().toString(36).slice(2)}.png`
221
+ const convertData = await sharp(fileBuffer).png().toBuffer()
222
+
223
+ await fs.promises.writeFile(`${tmpFolder}/${fileName}`, convertData)
224
+ res.send(`https://${req.get('host')}/file/${fileName}`)
225
+ } catch (e) {
226
+ console.log(e)
227
+ e = String(e)
228
+ res.status(500).json({ error: true, message: e === '[object Object]' ? 'Internal Server Error' : e })
229
+ }
230
  })
231
 
232
  app.all(['/webp2gif', '/webp2mp4'], async (req, res) => {
233
+ if (!['POST'].includes(req.method)) return res.status(405).json({ success: false, message: 'Method Not Allowed' })
234
+
235
+ try {
236
+ const { file } = req.body
237
+ if (!(file && isBase64(file))) return res.json({ success: false, message: 'Payload body file must be filled in base64 format' })
238
+
239
+ const fileBuffer = Buffer.from(file, 'base64')
240
+ const fileName = `${Math.random().toString(36).slice(2)}.webp`
241
+ const filePath = `${tmpFolder}/${fileName}`
242
+ await fs.promises.writeFile(filePath, fileBuffer)
243
+
244
+ const exec = util.promisify(cp.exec).bind(cp)
245
+ await exec(`convert ${filePath} ${filePath.replace('.webp', '.gif')}`)
246
+ if (/gif/.test(req.path)) return res.send(`https://${req.get('host')}/file/${fileName.replace('.webp', '.gif')}`)
247
+
248
+ await exec(`ffmpeg -i ${filePath.replace('.webp', '.gif')} -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ${filePath.replace(/.webp|.gif/g, '')}.mp4`)
249
+ res.send(`https://${req.get('host')}/file/${fileName.replace('.webp', '.mp4')}`)
250
+ } catch (e) {
251
+ console.log(e)
252
+ e = String(e)
253
+ res.status(500).json({ error: true, message: e === '[object Object]' ? 'Internal Server Error' : e })
254
+ }
255
  })
256
 
257
  app.get('/fetch', async (req, res) => {
258
+ try {
259
+ if (!req.query.url) return res.json({ message: 'Required an url' })
260
+ let json = await axios.get(req.query.url)
261
+ res.json(json.data)
262
+ } catch (e) {
263
+ res.send(e)
264
+ }
265
  })
266
 
267
 
 
270
 
271
  if (!targetUrl) {
272
  return res.status(400).json({
 
273
  status: false,
274
  message: 'Parameter "url" dibutuhkan',
275
  });
276
  }
277
 
278
  const baseUrl = new URL(targetUrl).origin;
279
+
280
  const browser = await playwright.chromium.launch({
281
  headless: true,
282
  executablePath: '/usr/bin/chromium', // pastikan path ini valid
 
309
  const content = await page.content();
310
 
311
  res.json({
 
312
  status: true,
313
  data: {
314
  results: [
 
319
 
320
  } catch (err) {
321
  res.status(500).json({
 
322
  status: false,
323
  message: 'Gagal fetch halaman',
324
  error: err.message
 
332
  app.listen(PORT, () => console.log('App running on port', PORT))
333
 
334
  function formatSize(num) {
335
+ return bytes(+num || 0, { unitSeparator: ' ' })
336
  }
337
 
338
  function isBase64(str) {
339
+ try {
340
+ return btoa(atob(str)) === str
341
+ } catch {
342
+ return false
343
+ }
344
  }
345
 
346
  function toPDF(urls) {
347
+ return new Promise(async (resolve, reject) => {
348
+ try {
349
+ if (!Array.isArray(urls)) urls = [urls]
350
+ const doc = new PDFDocument({ margin: 0, size: 'A4' })
351
+ const buffers = []
352
+
353
+ for (let i = 0; i < urls.length; i++) {
354
+ const response = await fetch(urls[i], { headers: { referer: urls[i] } })
355
+ if (!response.ok) continue
356
+
357
+ const type = response.headers.get('content-type')
358
+ if (!/image/.test(type)) continue
359
+
360
+ let buffer = Buffer.from(await response.arrayBuffer())
361
+ if (/gif|webp/.test(type)) buffer = await sharp(buffer).png().toBuffer()
362
+
363
+ doc.image(buffer, 0, 0, { fit: [595.28, 841.89], align: 'center', valign: 'center' })
364
+ if (urls.length !== i + 1) doc.addPage()
365
+ }
366
+
367
+ doc.on('data', (chunk) => buffers.push(chunk))
368
+ doc.on('end', () => resolve(Buffer.concat(buffers)))
369
+ doc.on('error', reject)
370
+ doc.end()
371
+ } catch (e) {
372
+ console.log(e)
373
+ reject(e)
374
+ }
375
+ })
376
  }