Aqso commited on
Commit
958957e
·
verified ·
1 Parent(s): 42f9683

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +48 -121
index.js CHANGED
@@ -2,6 +2,7 @@ const puppeteer = require('puppeteer-extra');
2
  const StealthPlugin = require('puppeteer-extra-plugin-stealth');
3
  const express = require('express');
4
  const admin = require('firebase-admin');
 
5
 
6
  puppeteer.use(StealthPlugin());
7
  const app = express();
@@ -17,85 +18,10 @@ if (serviceAccount.project_id) {
17
  }
18
  const db = admin.database();
19
 
20
- // GOD MODE UI (Obsidian Premium with Live Logs)
21
- const UI = (content, query = '', page = 1) => `
22
- <html><head>
23
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
25
- <style>
26
- :root { --primary: #3b82f6; --bg: #050505; --card: #111; --text: #f8fafc; }
27
- body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; margin: 0; padding-bottom: 90px; }
28
-
29
- header { background: rgba(5,5,5,0.9); backdrop-filter: blur(20px); padding: 12px 20px; border-bottom: 1px solid #222; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 1000; }
30
- .logo { font-weight: 900; font-size: 18px; color: #fff; text-decoration: none; }
31
- .logo span { color: var(--primary); }
32
-
33
- .search-input { background: #111; border: 1px solid #333; padding: 8px 15px; border-radius: 20px; color: #fff; font-size: 12px; outline: none; width: 130px; }
34
-
35
- .main { padding: 15px; max-width: 1200px; margin: 0 auto; }
36
-
37
- /* SYSTEM LOG BOX */
38
- #sys-log { background: #000; border: 1px solid #222; border-radius: 8px; padding: 10px; margin-bottom: 20px; font-family: monospace; font-size: 10px; color: #10b981; max-height: 80px; overflow-y: auto; display: none; }
39
 
40
- .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
41
- @media (min-width: 768px) { .grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 18px; } }
42
-
43
- .card { background: var(--card); border-radius: 12px; overflow: hidden; border: 1px solid #222; cursor: pointer; transition: 0.3s; position: relative; }
44
- .card img { width: 100%; height: 230px; object-fit: cover; background: #111; }
45
- .card-title { padding: 10px; font-size: 11px; font-weight: 700; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
46
- .card-status { position: absolute; top: 5px; right: 5px; background: rgba(0,0,0,0.8); color: var(--primary); font-size: 8px; padding: 2px 6px; border-radius: 4px; font-weight: 900; }
47
-
48
- .pagination { display: flex; justify-content: center; gap: 8px; margin: 25px 0; }
49
- .page-btn { background: #111; border: 1px solid #222; color: #94a3b8; padding: 8px 14px; border-radius: 8px; text-decoration: none; font-size: 11px; font-weight: bold; }
50
- .page-btn.active { background: var(--primary); color: #fff; border-color: var(--primary); }
51
-
52
- .bottom-nav { position: fixed; bottom: 0; left: 0; width: 100%; background: rgba(5,5,5,0.95); backdrop-filter: blur(20px); border-top: 1px solid #222; display: flex; justify-content: space-around; padding: 15px 0; z-index: 9999; }
53
- .nav-item { color: #4b5563; text-decoration: none; display: flex; flex-direction: column; align-items: center; gap: 5px; font-size: 10px; font-weight: bold; }
54
- .nav-item.active { color: var(--primary); }
55
-
56
- #loader { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 100000; flex-direction: column; justify-content: center; align-items: center; }
57
- </style>
58
- <script>
59
- function showLoading() { document.getElementById("loader").style.display = "flex"; }
60
- function startSync() {
61
- const u = prompt("Target (Anichin.cafe):", "https://anichin.cafe");
62
- if(u) {
63
- document.getElementById("sys-log").style.display = "block";
64
- fetch('/api/scrape?url=' + encodeURIComponent(u));
65
- monitor();
66
- }
67
- }
68
- async function monitor() {
69
- const r = await fetch('/api/status'); const d = await r.json();
70
- const log = document.getElementById("sys-log");
71
- log.innerHTML += "> " + d.msg + "<br>";
72
- log.scrollTop = log.scrollHeight;
73
- if(d.status === "running") setTimeout(monitor, 3000); else location.reload();
74
- }
75
- </script>
76
- </head><body>
77
- <div id="loader"><div style="border:3px solid #111; border-top:3px solid var(--primary); border-radius:50%; width:40px; height:40px; animation:spin 1s linear infinite;"></div><p id="st-text" style="color:var(--primary); margin-top:15px; font-size:11px; font-weight:bold;">EXECUTING COMMAND...</p></div>
78
- <header>
79
- <a href="/" class="logo">AQSO <span>OMEGA</span></a>
80
- <form action="/" style="margin:0;"><input type="text" name="q" value="${query}" class="search-input" placeholder="Search..."></form>
81
- <i class="fas fa-sync-alt" style="color:var(--primary); cursor:pointer;" onclick="startSync()"></i>
82
- </header>
83
- <div class="main">
84
- <div id="sys-log"></div>
85
- ${content}
86
- </div>
87
- <div class="bottom-nav">
88
- <a href="/" class="nav-item ${!query ? 'active' : ''}"><i class="fas fa-home"></i>HOME</a>
89
- <a href="/?page=1" class="nav-item"><i class="fas fa-database"></i>DATABASE</a>
90
- <a href="javascript:void(0)" onclick="startSync()" class="nav-item"><i class="fas fa-bolt"></i>SYNC</a>
91
- </div>
92
- <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
93
- </body></html>`;
94
-
95
- let s = { status: 'idle', msg: '' };
96
- app.get('/api/status', (req, res) => res.json(s));
97
-
98
- // 1. DASHBOARD WITH FALLBACK
99
  app.get('/', async (req, res) => {
100
  const q = req.query.q?.toLowerCase() || '';
101
  const page = parseInt(req.query.page) || 1;
@@ -104,104 +30,105 @@ app.get('/', async (req, res) => {
104
  const snapshot = await db.ref('donghua').once('value');
105
  const data = snapshot.val() || {};
106
 
107
- // Cuma tampilkan yang ada posternya
108
  let items = Object.keys(data).map(k => ({ id: k, ...data[k] }))
109
- .filter(i => i.img);
110
 
111
  if (q) items = items.filter(i => i.title.toLowerCase().includes(q));
112
 
 
113
  const paginated = items.reverse().slice((page - 1) * limit, page * limit);
 
114
  let grid = `<div class="grid">`;
115
-
116
  paginated.forEach(i => {
117
- const epReady = (i.episodes?.length > 0);
118
- grid += `<div class="card" onclick="${epReady ? "showLoading(); location.href='/view?id='+this.dataset.id" : "alert('Eps Belum Siap!')"}" data-id="${i.id}">
119
- ${!epReady ? '<div class="card-status">DOWNLOADING...</div>' : ''}
120
  <img src="${i.img}"><div class="card-title">${i.title}</div></div>`;
121
  });
122
  grid += '</div>';
123
 
124
  let pages = '<div class="pagination">';
125
- for (let i = 1; i <= Math.ceil(items.length / limit); i++) {
126
  pages += `<a href="/?page=${i}${q ? '&q='+q : ''}" class="page-btn ${page === i ? 'active' : ''}">${i}</a>`;
127
  }
128
  pages += '</div>';
129
 
130
- res.send(UI(grid + (items.length > limit ? pages : '')));
131
  });
132
 
133
- // 2. GOD SCRAPER (Aggressive & Honest)
134
  app.get('/api/scrape', async (req, res) => {
135
  const { url } = req.query;
136
- s = { status: 'running', msg: 'Checking Firebase Connection...' };
137
  let browser;
138
  try {
139
  browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
140
  const page = await browser.newPage();
141
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 90000 });
142
- await page.waitForFunction(() => !document.body.innerText.includes('7 detik'), { timeout: 30000 }).catch(() => {});
143
 
144
- s.msg = 'Home Page Captured. Extracting Titles...';
145
  const list = await page.evaluate(() => {
146
- return Array.from(document.querySelectorAll('.bs, .article')).map(el => {
147
- const img = el.querySelector('img');
148
- return {
149
- title: el.querySelector('h2, .title, .entry-title')?.innerText,
150
- img: img?.src || img?.dataset?.src || img?.dataset?.lazySrc || img?.srcset?.split(' ')[0],
151
- url: el.querySelector('a')?.href
152
- };
153
- }).filter(i => i.title && i.url);
154
  });
155
 
156
  for(let i=0; i < list.length; i++) {
157
  const key = Buffer.from(list[i].title).toString('hex').slice(0, 30);
158
 
159
- // Masukkan Judul & Poster dulu biar gak "KOSONG"
160
- await db.ref('donghua/' + key).update({ title: list[i].title, img: list[i].img, url: list[i].url });
161
-
162
- s.msg = `Injected Poster: ${list[i].title}`;
 
 
 
 
163
  const dp = await browser.newPage();
164
  try {
165
- await dp.goto(list[i].url, { waitUntil: 'domcontentloaded', timeout: 60000 });
166
- await dp.waitForSelector('.eplister, .list-episode, .cl-item', { timeout: 15000 }).catch(() => {});
167
 
168
  const episodes = await dp.evaluate(() => {
169
- return Array.from(document.querySelectorAll('.eplister li, .list-episode li, .cl-item')).map(li => ({
170
  num: li.querySelector('.epl-num, .num')?.innerText || '?',
171
  url: li.querySelector('a')?.href
172
  })).filter(e => e.url);
173
  }) || [];
174
 
175
  if (episodes.length > 0) {
176
- await db.ref('donghua/' + key).update({ episodes, lastSync: Date.now() });
177
- s.msg = `SUCCESS: ${list[i].title} (${episodes.length} Eps)`;
178
  } else {
179
- s.msg = `FAILED: No episodes found for ${list[i].title}`;
180
  }
181
- } catch (e) { s.msg = `ERROR Detail: ${list[i].title}`; }
 
 
182
  await dp.close();
183
  }
184
- s = { status: 'done', msg: 'System Overlord Complete.' };
185
- } catch (e) { s = { status: 'error', msg: e.message }; }
186
  finally { if(browser) await browser.close(); }
187
  });
188
 
189
- // 3. VIEW DETAIL
190
  app.get('/view', async (req, res) => {
191
  const snapshot = await db.ref('donghua/' + req.query.id).once('value');
192
  const item = snapshot.val();
193
- if(!item || !item.episodes) return res.send("Data Belum Siap!");
194
 
195
- let list = `<div style="margin-bottom:20px; text-align:center;">
196
- <img src="${item.img}" style="width:140px; border-radius:12px; border:1px solid #222;">
197
- <h2 style="margin:10px 0;">${item.title}</h2>
198
  </div>`;
199
 
200
  item.episodes.forEach(e => {
201
  list += `<a href="/player?url=${encodeURIComponent(e.url)}" class="page-btn" style="display:block; margin-bottom:10px; text-align:left; padding:15px;">
202
- <i class="fas fa-play" style="margin-right:12px; color:var(--primary);"></i> Episode ${e.num}</a>`;
203
  });
204
- res.send(UI(list));
205
  });
206
 
207
  // 4. PLAYER
@@ -213,8 +140,8 @@ app.get('/player', async (req, res) => {
213
  const page = await browser.newPage();
214
  await page.goto(url, { waitUntil: 'networkidle2' });
215
  const player = await page.evaluate(() => document.querySelector('iframe, .video-content iframe')?.src);
216
- res.send(UI(`<div style="background:#000; height:60vh; border-radius:15px; overflow:hidden;"><iframe src="${player}" style="width:100%; height:100%; border:none;" allowfullscreen></iframe></div>`));
217
- } catch (e) { res.send(UI("Error Player: " + e.message)); }
218
  finally { if(browser) await browser.close(); }
219
  });
220
 
 
2
  const StealthPlugin = require('puppeteer-extra-plugin-stealth');
3
  const express = require('express');
4
  const admin = require('firebase-admin');
5
+ const views = require('./views'); // Panggil UI
6
 
7
  puppeteer.use(StealthPlugin());
8
  const app = express();
 
18
  }
19
  const db = admin.database();
20
 
21
+ let scrapeStatus = { status: 'idle', msg: '' };
22
+ app.get('/api/status', (req, res) => res.json(scrapeStatus));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ // 1. DASHBOARD: Hanya munculkan yang READY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  app.get('/', async (req, res) => {
26
  const q = req.query.q?.toLowerCase() || '';
27
  const page = parseInt(req.query.page) || 1;
 
30
  const snapshot = await db.ref('donghua').once('value');
31
  const data = snapshot.val() || {};
32
 
33
+ // Filter: Sudah ada episodes
34
  let items = Object.keys(data).map(k => ({ id: k, ...data[k] }))
35
+ .filter(i => i.episodes && i.episodes.length > 0 && i.img);
36
 
37
  if (q) items = items.filter(i => i.title.toLowerCase().includes(q));
38
 
39
+ const totalPages = Math.ceil(items.length / limit);
40
  const paginated = items.reverse().slice((page - 1) * limit, page * limit);
41
+
42
  let grid = `<div class="grid">`;
 
43
  paginated.forEach(i => {
44
+ grid += `<div class="card" onclick="location.href='/view?id=${i.id}'">
 
 
45
  <img src="${i.img}"><div class="card-title">${i.title}</div></div>`;
46
  });
47
  grid += '</div>';
48
 
49
  let pages = '<div class="pagination">';
50
+ for (let i = 1; i <= totalPages; i++) {
51
  pages += `<a href="/?page=${i}${q ? '&q='+q : ''}" class="page-btn ${page === i ? 'active' : ''}">${i}</a>`;
52
  }
53
  pages += '</div>';
54
 
55
+ res.send(views.layout(grid + (totalPages > 1 ? pages : ''), q, page));
56
  });
57
 
58
+ // 2. LIVE SCRAPER: Smart Check & One-by-One
59
  app.get('/api/scrape', async (req, res) => {
60
  const { url } = req.query;
61
+ scrapeStatus = { status: 'running', msg: 'Awakening Blitz Engine...' };
62
  let browser;
63
  try {
64
  browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
65
  const page = await browser.newPage();
66
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
67
+ await page.waitForFunction(() => !document.body.innerText.includes('7 detik'), { timeout: 20000 }).catch(() => {});
68
 
 
69
  const list = await page.evaluate(() => {
70
+ return Array.from(document.querySelectorAll('.bs, .article')).map(el => ({
71
+ title: el.querySelector('h2, .title, .entry-title')?.innerText,
72
+ img: el.querySelector('img')?.src || el.querySelector('img')?.dataset?.src,
73
+ url: el.querySelector('a')?.href
74
+ })).filter(i => i.title && i.url);
 
 
 
75
  });
76
 
77
  for(let i=0; i < list.length; i++) {
78
  const key = Buffer.from(list[i].title).toString('hex').slice(0, 30);
79
 
80
+ // LOGIKA SMART: Cek DB dulu
81
+ const check = await db.ref('donghua/' + key).once('value');
82
+ if (check.val()?.episodes?.length > 0) {
83
+ scrapeStatus.msg = `Skip: ${list[i].title} (Ready)`;
84
+ continue;
85
+ }
86
+
87
+ scrapeStatus.msg = `Installing (${i+1}/${list.length}): ${list[i].title}`;
88
  const dp = await browser.newPage();
89
  try {
90
+ await dp.goto(list[i].url, { waitUntil: 'domcontentloaded', timeout: 30000 });
91
+ await dp.waitForSelector('.eplister, .list-episode', { timeout: 10000 }).catch(() => {});
92
 
93
  const episodes = await dp.evaluate(() => {
94
+ return Array.from(document.querySelectorAll('.eplister li, .list-episode li')).map(li => ({
95
  num: li.querySelector('.epl-num, .num')?.innerText || '?',
96
  url: li.querySelector('a')?.href
97
  })).filter(e => e.url);
98
  }) || [];
99
 
100
  if (episodes.length > 0) {
101
+ // Inject satu-persatu
102
+ await db.ref('donghua/' + key).set({ ...list[i], episodes, lastSync: Date.now() });
103
  } else {
104
+ await db.ref('donghua/' + key).remove(); // Hapus jika error
105
  }
106
+ } catch (e) {
107
+ await db.ref('donghua/' + key).remove();
108
+ }
109
  await dp.close();
110
  }
111
+ scrapeStatus = { status: 'done', msg: 'Sync Majestic Selesai!' };
112
+ } catch (e) { scrapeStatus = { status: 'error', msg: e.message }; }
113
  finally { if(browser) await browser.close(); }
114
  });
115
 
116
+ // 3. VIEW DETAIL & MULTI-SERVER
117
  app.get('/view', async (req, res) => {
118
  const snapshot = await db.ref('donghua/' + req.query.id).once('value');
119
  const item = snapshot.val();
120
+ if(!item) return res.redirect('/');
121
 
122
+ let list = `<div style="display:flex; gap:15px; margin-bottom:20px; align-items:center;">
123
+ <img src="${item.img}" style="width:100px; height:140px; border-radius:10px; object-fit:cover;">
124
+ <div><h2>${item.title}</h2><p style="color:var(--primary); font-weight:800;">${item.episodes.length} EPS READY</p></div>
125
  </div>`;
126
 
127
  item.episodes.forEach(e => {
128
  list += `<a href="/player?url=${encodeURIComponent(e.url)}" class="page-btn" style="display:block; margin-bottom:10px; text-align:left; padding:15px;">
129
+ <i class="fas fa-play" style="margin-right:12px;"></i> Episode ${e.num}</a>`;
130
  });
131
+ res.send(views.layout(list));
132
  });
133
 
134
  // 4. PLAYER
 
140
  const page = await browser.newPage();
141
  await page.goto(url, { waitUntil: 'networkidle2' });
142
  const player = await page.evaluate(() => document.querySelector('iframe, .video-content iframe')?.src);
143
+ res.send(views.layout(`<div style="background:#000; height:60vh; border-radius:15px; overflow:hidden;"><iframe src="${player}" style="width:100%; height:100%; border:none;" allowfullscreen></iframe></div>`));
144
+ } catch (e) { res.send(views.layout("Error: " + e.message)); }
145
  finally { if(browser) await browser.close(); }
146
  });
147