devusman commited on
Commit
afab07a
Β·
1 Parent(s): dee4c6e
Files changed (2) hide show
  1. index.html +171 -153
  2. server.js +89 -108
index.html CHANGED
@@ -87,7 +87,7 @@
87
  min-width: 120px;
88
  }
89
 
90
- #download-btn:hover:not(:disabled) {
91
  background-color: #0056b3;
92
  }
93
 
@@ -98,8 +98,8 @@
98
 
99
  .btn-loader {
100
  display: none;
101
- border: 3px solid rgba(255, 255, 255, 0.3);
102
- border-top: 3px solid #ffffff;
103
  border-radius: 50%;
104
  width: 20px;
105
  height: 20px;
@@ -124,80 +124,102 @@
124
  }
125
  }
126
 
127
- .status-indicator {
128
- padding: 12px;
129
- border-radius: 8px;
130
- margin-top: 20px;
131
- font-size: 0.95rem;
132
- display: block;
133
  text-align: left;
134
  }
135
 
136
- .status-indicator.success {
137
- background-color: #d4edda;
138
- color: #155724;
139
- border: 1px solid #c3e6cb;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
  .status-indicator.error {
143
  background-color: #f8d7da;
144
  color: #721c24;
145
  border: 1px solid #f5c6cb;
 
 
 
 
146
  }
147
 
148
- .status-indicator.info {
149
- background-color: #cce5ff;
150
- color: #004085;
151
- border: 1px solid #b8daff;
152
- }
153
-
154
- /* New styles for the log container */
155
- .log-container {
156
- margin-top: 20px;
157
  border: 1px solid #dfe4ea;
158
  border-radius: 8px;
159
- text-align: left;
160
- background-color: #f8f9fa;
161
  }
162
 
163
- .log-container h3 {
164
- margin: 0;
165
- padding: 12px 15px;
166
- background-color: #e9ecef;
167
- border-bottom: 1px solid #dfe4ea;
168
  font-size: 1rem;
169
  font-weight: 600;
170
- color: #495057;
171
  }
172
 
173
- #log-output {
174
- margin: 0;
175
- padding: 15px;
176
- height: 200px;
177
- overflow-y: auto;
178
- font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
179
  font-size: 0.85rem;
180
- color: #333;
181
- line-height: 1.6;
 
 
182
  white-space: pre-wrap;
183
  word-wrap: break-word;
184
  }
185
 
186
  .log-entry {
187
- margin: 0;
188
- padding: 0;
 
 
 
 
 
 
 
 
 
 
189
  }
190
 
191
- .log-entry.error {
192
- color: #d9534f;
193
- font-weight: bold;
194
  }
195
 
196
- .log-entry.success {
197
- color: #5cb85c;
198
- font-weight: bold;
199
  }
200
 
 
 
201
  .footer {
202
  margin-top: 30px;
203
  font-size: 0.8rem;
@@ -224,155 +246,151 @@
224
  <span class="btn-loader"></span>
225
  </button>
226
  </div>
227
- <div class="status-indicator" id="status-indicator" style="display: none;"></div>
228
 
229
- <!-- New Log Container -->
230
- <div class="log-container" id="log-container" style="display: none;">
231
- <h3>Session Logs</h3>
232
- <pre id="log-output"></pre>
 
 
 
 
 
 
 
233
  </div>
 
 
234
  </div>
235
  <div class="footer">
236
  <p>Powered by the Heart by Us</p>
237
  </div>
238
  </div>
239
-
240
  <script>
241
  document.addEventListener('DOMContentLoaded', () => {
242
  const downloadBtn = document.getElementById('download-btn');
243
  const urlInput = document.getElementById('studocu-url');
244
- const statusIndicator = document.getElementById('status-indicator');
 
 
 
 
 
245
  const logContainer = document.getElementById('log-container');
246
- const logOutput = document.getElementById('log-output');
247
 
248
- const API_URL = 'http://localhost:7860';
249
- let eventSource;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  downloadBtn.addEventListener('click', async () => {
252
  const url = urlInput.value.trim();
253
  if (!url || !url.includes('studocu.com')) {
254
- showStatus('Please provide a valid StuDocu URL.', 'error');
255
  return;
256
  }
257
 
 
258
  setLoading(true);
259
- clearLogs();
260
- showStatus('Initializing session...', 'info');
 
 
261
 
262
  try {
263
- // Step 1: Start the download process on the server
264
- const response = await fetch(`${API_URL}/api/download`, {
265
  method: 'POST',
266
  headers: { 'Content-Type': 'application/json' },
267
- body: JSON.stringify({ url: url }),
268
  });
269
 
270
  if (!response.ok) {
271
  const errorData = await response.json();
272
- throw new Error(errorData.error || 'Failed to start download process.');
273
  }
274
 
275
- const data = await response.json();
276
- const sessionId = data.sessionId;
277
-
278
- // Step 2: Listen for progress updates using the received sessionId
279
- setupEventListener(sessionId, url);
280
 
281
  } catch (error) {
282
- const message = `Error: ${error.message}`;
283
- addLogEntry(`❌ ${message}`, 'error');
284
- showStatus(message, 'error');
285
  setLoading(false);
286
  }
287
  });
288
 
289
- function setupEventListener(sessionId, originalUrl) {
290
- // The URL is correct in your original code
291
- eventSource = new EventSource(`${API_URL}/api/progress-stream/${sessionId}`);
292
-
293
- eventSource.onopen = () => {
294
- addLogEntry('πŸ”Œ Connection established. Waiting for server...');
295
- };
296
-
297
- eventSource.onmessage = (event) => {
298
- const data = JSON.parse(event.data);
299
-
300
- if (data.status === 'error') {
301
- const message = `Error: ${data.message}`;
302
- addLogEntry(`❌ ${message}`, 'error');
303
- showStatus(message, 'error');
304
- eventSource.close();
305
- setLoading(false);
306
- return;
307
- }
308
-
309
- const logMessage = `[${data.progress}%] ${data.status}: ${data.message}`;
310
- addLogEntry(logMessage);
311
- showStatus(`${data.status}...`, 'info');
312
-
313
- if (data.progress === 100) {
314
- addLogEntry('πŸŽ‰ PDF generated successfully! Starting download...', 'success');
315
- showStatus('Download complete!', 'success');
316
-
317
- // The backend doesn't send the file anymore, so we trigger a direct download.
318
- // For this simple fix, we will just re-post to the original endpoint
319
- // but this time to a new one that returns the PDF directly.
320
- // (This part requires another backend change, for now let's just log it)
321
-
322
- // A simple way to get the file is to have a new download endpoint
323
- // For now, we assume the process is done.
324
- eventSource.close();
325
- setLoading(false);
326
- // To actually download, you would need another endpoint like GET /api/download-file/:sessionId
327
- // For simplicity, we just stop here.
328
- }
329
- };
330
-
331
- eventSource.onerror = (err) => {
332
- console.error("EventSource failed:", err);
333
- addLogEntry('❌ Connection to server was lost.', 'error');
334
- showStatus('Connection lost. Please try again.', 'error');
335
- eventSource.close();
336
- setLoading(false);
337
- };
338
- }
339
-
340
  function setLoading(isLoading) {
341
- downloadBtn.classList.toggle('loading', isLoading);
342
  downloadBtn.disabled = isLoading;
343
  urlInput.disabled = isLoading;
 
344
  }
345
 
346
- function showStatus(message, type) {
347
- statusIndicator.style.display = 'block';
348
- statusIndicator.textContent = message;
349
- statusIndicator.className = `status-indicator ${type}`;
350
- }
351
-
352
- function addLogEntry(message, type = 'info') {
353
- const entry = document.createElement('p');
354
- entry.className = `log-entry ${type}`;
355
- entry.textContent = message;
356
- logOutput.appendChild(entry);
357
- logOutput.scrollTop = logOutput.scrollHeight;
358
- }
359
-
360
- function clearLogs() {
361
- logContainer.style.display = 'block';
362
- logOutput.innerHTML = '';
363
- }
364
-
365
- // This function is no longer called in the same way, but kept for reference
366
- function triggerFileDownload(blob, fileName) {
367
- const downloadUrl = window.URL.createObjectURL(blob);
368
- const a = document.createElement('a');
369
- a.style.display = 'none';
370
- a.href = downloadUrl;
371
- a.download = fileName;
372
- document.body.appendChild(a);
373
- a.click();
374
- window.URL.revokeObjectURL(downloadUrl);
375
- a.remove();
376
  }
377
  });
378
  </script>
 
87
  min-width: 120px;
88
  }
89
 
90
+ #download-btn:hover {
91
  background-color: #0056b3;
92
  }
93
 
 
98
 
99
  .btn-loader {
100
  display: none;
101
+ border: 3px solid #f3f3f3;
102
+ border-top: 3px solid #0056b3;
103
  border-radius: 50%;
104
  width: 20px;
105
  height: 20px;
 
124
  }
125
  }
126
 
127
+ .progress-section {
128
+ margin-top: 25px;
 
 
 
 
129
  text-align: left;
130
  }
131
 
132
+ .progress-bar-container {
133
+ width: 100%;
134
+ background-color: #e9ecef;
135
+ border-radius: 8px;
136
+ overflow: hidden;
137
+ height: 12px;
138
+ }
139
+
140
+ .progress-bar {
141
+ width: 0%;
142
+ height: 100%;
143
+ background-color: #007bff;
144
+ border-radius: 8px;
145
+ transition: width 0.4s ease-in-out;
146
+ }
147
+
148
+ #status-text {
149
+ margin-top: 8px;
150
+ color: #495057;
151
+ font-size: 0.9rem;
152
+ text-align: center;
153
  }
154
 
155
  .status-indicator.error {
156
  background-color: #f8d7da;
157
  color: #721c24;
158
  border: 1px solid #f5c6cb;
159
+ padding: 12px;
160
+ border-radius: 8px;
161
+ margin-top: 20px;
162
+ font-size: 0.95rem;
163
  }
164
 
165
+ /* --- NEW LOG CONTAINER STYLES --- */
166
+ .log-section {
167
+ margin-top: 25px;
168
+ text-align: left;
 
 
 
 
 
169
  border: 1px solid #dfe4ea;
170
  border-radius: 8px;
171
+ padding: 15px;
172
+ background-color: #fafafa;
173
  }
174
 
175
+ .log-section h3 {
176
+ margin-top: 0;
177
+ margin-bottom: 10px;
178
+ color: #2c3e50;
 
179
  font-size: 1rem;
180
  font-weight: 600;
 
181
  }
182
 
183
+ .log-container {
184
+ background-color: #2c3e50;
185
+ color: #ecf0f1;
186
+ font-family: 'Courier New', Courier, monospace;
 
 
187
  font-size: 0.85rem;
188
+ height: 150px;
189
+ overflow-y: auto;
190
+ padding: 10px;
191
+ border-radius: 6px;
192
  white-space: pre-wrap;
193
  word-wrap: break-word;
194
  }
195
 
196
  .log-entry {
197
+ display: flex;
198
+ margin-bottom: 5px;
199
+ }
200
+
201
+ .log-entry .timestamp {
202
+ color: #95a5a6;
203
+ margin-right: 10px;
204
+ flex-shrink: 0;
205
+ }
206
+
207
+ .log-entry .message {
208
+ flex-grow: 1;
209
  }
210
 
211
+ .log-entry.error .message {
212
+ color: #e74c3c;
213
+ /* Red */
214
  }
215
 
216
+ .log-entry.success .message {
217
+ color: #2ecc71;
218
+ /* Green */
219
  }
220
 
221
+ /* --- END OF NEW STYLES --- */
222
+
223
  .footer {
224
  margin-top: 30px;
225
  font-size: 0.8rem;
 
246
  <span class="btn-loader"></span>
247
  </button>
248
  </div>
 
249
 
250
+ <div class="progress-section" id="progress-section" style="display: none;">
251
+ <div class="progress-bar-container">
252
+ <div class="progress-bar" id="progress-bar"></div>
253
+ </div>
254
+ <p id="status-text">Starting...</p>
255
+ </div>
256
+
257
+ <!-- NEW: Log Display Area -->
258
+ <div class="log-section" id="log-section" style="display: none;">
259
+ <h3>Live Logs</h3>
260
+ <div class="log-container" id="log-container"></div>
261
  </div>
262
+
263
+ <div class="status-indicator error" id="error-indicator" style="display: none;"></div>
264
  </div>
265
  <div class="footer">
266
  <p>Powered by the Heart by Us</p>
267
  </div>
268
  </div>
 
269
  <script>
270
  document.addEventListener('DOMContentLoaded', () => {
271
  const downloadBtn = document.getElementById('download-btn');
272
  const urlInput = document.getElementById('studocu-url');
273
+ const progressSection = document.getElementById('progress-section');
274
+ const progressBar = document.getElementById('progress-bar');
275
+ const statusText = document.getElementById('status-text');
276
+ const errorIndicator = document.getElementById('error-indicator');
277
+ // NEW: Get log elements
278
+ const logSection = document.getElementById('log-section');
279
  const logContainer = document.getElementById('log-container');
 
280
 
281
+ const API_BASE_URL = 'http://localhost:7860';
282
+ let pollInterval;
283
+ let lastLoggedMessage = ''; // NEW: Track last message to avoid duplicates
284
+
285
+ const resetUI = () => {
286
+ setLoading(false);
287
+ progressSection.style.display = 'none';
288
+ errorIndicator.style.display = 'none';
289
+ progressBar.style.width = '0%';
290
+ statusText.textContent = '';
291
+ // NEW: Reset log area
292
+ logSection.style.display = 'none';
293
+ logContainer.innerHTML = '';
294
+ lastLoggedMessage = '';
295
+ };
296
+
297
+ // NEW: Function to add a message to the log container
298
+ const addLog = (message, type = 'info') => {
299
+ if (!message || message === lastLoggedMessage) return; // Avoid empty or duplicate logs
300
+ lastLoggedMessage = message;
301
+
302
+ const logEntry = document.createElement('div');
303
+ logEntry.className = `log-entry ${type}`;
304
+ const timestamp = new Date().toLocaleTimeString();
305
+ logEntry.innerHTML = `<span class="timestamp">${timestamp}</span><span class="message">${message}</span>`;
306
+
307
+ logContainer.appendChild(logEntry);
308
+ logContainer.scrollTop = logContainer.scrollHeight; // Auto-scroll
309
+ };
310
+
311
+ const pollProgress = (sessionId) => {
312
+ pollInterval = setInterval(async () => {
313
+ try {
314
+ const response = await fetch(`${API_BASE_URL}/api/progress/${sessionId}`);
315
+ if (!response.ok) throw new Error('Failed to get progress update.');
316
+
317
+ const data = await response.json();
318
+
319
+ progressBar.style.width = `${data.progress}%`;
320
+ statusText.textContent = `(${data.progress}%) ${data.message}`;
321
+ addLog(`[${data.status}] ${data.message}`); // Add to log
322
+
323
+ if (data.progress >= 100) {
324
+ clearInterval(pollInterval);
325
+ statusText.textContent = 'βœ… Success! Your download will start now.';
326
+ addLog('βœ… PDF generated successfully! Starting download...', 'success');
327
+ window.location.href = `${API_BASE_URL}/api/download/${sessionId}`;
328
+ setTimeout(resetUI, 5000);
329
+ }
330
+
331
+ if (data.progress < 0) {
332
+ clearInterval(pollInterval);
333
+ showError(`Error: ${data.message || 'An unknown error occurred.'}`);
334
+ setLoading(false);
335
+ }
336
+
337
+ } catch (error) {
338
+ clearInterval(pollInterval);
339
+ console.error('Polling failed:', error);
340
+ showError('Failed to connect to the server for progress updates.');
341
+ setLoading(false);
342
+ }
343
+ }, 2000);
344
+ };
345
 
346
  downloadBtn.addEventListener('click', async () => {
347
  const url = urlInput.value.trim();
348
  if (!url || !url.includes('studocu.com')) {
349
+ showError('Please provide a valid StuDocu URL.');
350
  return;
351
  }
352
 
353
+ resetUI();
354
  setLoading(true);
355
+ progressSection.style.display = 'block';
356
+ logSection.style.display = 'block'; // Show logs
357
+ statusText.textContent = 'Requesting download...';
358
+ addLog('πŸš€ Requesting download...');
359
 
360
  try {
361
+ const response = await fetch(`${API_BASE_URL}/api/request-download`, {
 
362
  method: 'POST',
363
  headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify({ url }),
365
  });
366
 
367
  if (!response.ok) {
368
  const errorData = await response.json();
369
+ throw new Error(errorData.error || 'Failed to start the download process.');
370
  }
371
 
372
+ const { sessionId } = await response.json();
373
+ pollProgress(sessionId);
 
 
 
374
 
375
  } catch (error) {
376
+ console.error('Download failed:', error);
377
+ showError(error.message);
 
378
  setLoading(false);
379
  }
380
  });
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  function setLoading(isLoading) {
 
383
  downloadBtn.disabled = isLoading;
384
  urlInput.disabled = isLoading;
385
+ downloadBtn.classList.toggle('loading', isLoading);
386
  }
387
 
388
+ function showError(message) {
389
+ errorIndicator.style.display = 'block';
390
+ errorIndicator.textContent = message;
391
+ progressSection.style.display = 'none';
392
+ logSection.style.display = 'block'; // Ensure logs are visible on error
393
+ addLog(message, 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  }
395
  });
396
  </script>
server.js CHANGED
@@ -9,7 +9,13 @@ const port = 7860;
9
  app.use(cors());
10
  app.use(express.json());
11
 
12
- // Progress tracking system
 
 
 
 
 
 
13
  class ProgressTracker extends EventEmitter {
14
  constructor(sessionId) {
15
  super();
@@ -23,25 +29,20 @@ class ProgressTracker extends EventEmitter {
23
  this.progress = progress;
24
  this.status = status;
25
  this.message = message;
26
- const progressData = {
27
  sessionId: this.sessionId,
28
  progress,
29
  status,
30
  message,
31
  timestamp: new Date().toISOString()
32
  };
33
- // Emit the 'progress' event for SSE listeners
34
- this.emit('progress', progressData);
35
  console.log(`πŸ“Š [${this.sessionId}] ${progress}% - ${status}: ${message}`);
36
  }
37
  }
38
 
39
- // Store active progress trackers
40
- const progressTrackers = new Map();
41
 
42
- /**
43
- * Advanced cookie banner and content bypass for StuDocu
44
- */
45
  const bypassCookiesAndRestrictions = async (page, progressTracker) => {
46
  progressTracker?.updateProgress(5, 'bypassing', 'Setting up cookie bypass...');
47
 
@@ -160,10 +161,6 @@ const bypassCookiesAndRestrictions = async (page, progressTracker) => {
160
  progressTracker?.updateProgress(10, 'bypassing', 'Cookie bypass configured successfully');
161
  return true;
162
  };
163
-
164
- /**
165
- * Enhanced content unblurring and premium bypass
166
- */
167
  const unblurContent = async (page, progressTracker) => {
168
  progressTracker?.updateProgress(15, 'unblurring', 'Removing content restrictions...');
169
 
@@ -222,10 +219,6 @@ const unblurContent = async (page, progressTracker) => {
222
 
223
  progressTracker?.updateProgress(20, 'unblurring', 'Content restrictions removed');
224
  };
225
-
226
- /**
227
- * Apply print styles for clean PDF output
228
- */
229
  const applyPrintStyles = async (page, progressTracker) => {
230
  progressTracker?.updateProgress(85, 'styling', 'Applying print styles...');
231
 
@@ -282,10 +275,6 @@ const applyPrintStyles = async (page, progressTracker) => {
282
 
283
  progressTracker?.updateProgress(88, 'styling', 'Print styles applied successfully');
284
  };
285
-
286
- /**
287
- * Enhanced StuDocu downloader with progress tracking
288
- */
289
  const studocuDownloader = async (url, options = {}, progressTracker = null) => {
290
  let browser;
291
  try {
@@ -550,117 +539,108 @@ const studocuDownloader = async (url, options = {}, progressTracker = null) => {
550
  }
551
  };
552
 
 
553
 
554
- // API Routes
555
-
556
- // Enhanced download endpoint with progress tracking
557
- app.post('/api/download', async (req, res) => {
558
- // Note: The original client code was sending sessionId in the body.
559
- // We will use the one passed in the body or generate a new one.
560
- const { url, filename, email, password, sessionId: reqSessionId } = req.body;
561
- if (!url) {
562
- return res.status(400).json({ error: 'URL is required.' });
563
- }
564
- if (!url.includes('studocu.com')) {
565
  return res.status(400).json({ error: 'Please provide a valid StuDocu URL.' });
566
  }
567
 
568
- let normalizedUrl = url.trim();
569
- if (!normalizedUrl.startsWith('http')) {
570
- normalizedUrl = 'https://' + normalizedUrl;
571
- }
572
-
573
- // Use the session ID from the request or create a new one.
574
- const sessionId = reqSessionId || Date.now().toString();
575
  const progressTracker = new ProgressTracker(sessionId);
 
576
  progressTrackers.set(sessionId, progressTracker);
 
577
 
578
- console.log(`🎯 Processing request for: ${normalizedUrl} [Session: ${sessionId}]`);
579
 
580
- // We don't wait for the downloader to finish.
581
- // It runs in the background while we immediately return a response.
582
- studocuDownloader(normalizedUrl, { filename, email, password }, progressTracker)
 
 
583
  .then(pdfBuffer => {
584
- // Store the result for the user to download later or handle as needed
585
- progressTracker.pdfBuffer = pdfBuffer;
586
- console.log(`πŸŽ‰ PDF is ready for download [Session: ${sessionId}]`);
587
  })
588
  .catch(error => {
589
- console.error(`❌ Failed to process ${normalizedUrl}:`, error.message);
590
- // You can emit a final error event here if you want
591
- progressTracker.updateProgress(-1, 'error', error.message || 'An unknown error occurred.');
592
- })
593
- .finally(() => {
594
- // Optional: Clean up the tracker after some time
595
- setTimeout(() => {
596
- const tracker = progressTrackers.get(sessionId);
597
- // Don't delete if there's a PDF buffer waiting to be downloaded
598
- if (tracker && !tracker.pdfBuffer) {
599
- progressTrackers.delete(sessionId);
600
- }
601
- }, 300000); // 5 minutes
602
  });
603
-
604
- // Immediately respond to the client so it can start listening to the progress stream.
605
- res.status(202).json({
606
- message: "Download process started.",
607
- sessionId: sessionId
608
- });
609
  });
610
 
611
- // ***************************************************************
612
- // ** NEW SERVER-SENT EVENTS (SSE) ENDPOINT FOR REAL-TIME PROGRESS **
613
- // ***************************************************************
614
- app.get('/api/progress-stream/:sessionId', (req, res) => {
 
615
  const { sessionId } = req.params;
616
  const tracker = progressTrackers.get(sessionId);
617
 
618
- if (!tracker) {
619
- return res.status(404).json({ error: 'Session not found' });
 
 
 
 
 
 
 
620
  }
621
 
622
- // Set headers for SSE
623
- res.setHeader('Content-Type', 'text/event-stream');
624
- res.setHeader('Cache-Control', 'no-cache');
625
- res.setHeader('Connection', 'keep-alive');
626
- res.flushHeaders(); // Flush the headers to establish the connection
627
-
628
- // The function that sends data to the client
629
- const sendProgress = (data) => {
630
- res.write(`data: ${JSON.stringify(data)}\n\n`);
631
- };
632
-
633
- // Attach the listener to the specific tracker instance
634
- tracker.on('progress', sendProgress);
635
-
636
- // Handle client disconnect
637
- req.on('close', () => {
638
- // Remove the listener for this specific client
639
- tracker.removeListener('progress', sendProgress);
640
- console.log(`πŸ”Œ Client disconnected for session: ${sessionId}`);
641
- });
642
  });
643
 
644
- // Your old polling endpoint (can be kept for debugging or removed)
645
- app.get('/api/progress/:sessionId', (req, res) => {
 
 
 
646
  const { sessionId } = req.params;
647
- const tracker = progressTrackers.get(sessionId);
648
 
649
- if (!tracker) {
650
- return res.status(404).json({ error: 'Session not found' });
651
  }
652
 
653
- res.json({
654
- sessionId,
655
- progress: tracker.progress,
656
- status: tracker.status,
657
- message: tracker.message,
658
- timestamp: new Date().toISOString()
659
- });
 
 
 
 
 
 
 
 
 
 
660
  });
661
 
662
 
663
- // Health and info endpoints
664
  app.get('/health', (req, res) => {
665
  res.json({
666
  status: 'healthy',
@@ -672,8 +652,8 @@ app.get('/health', (req, res) => {
672
 
673
  app.get('/', (req, res) => {
674
  res.json({
675
- message: 'πŸš€ Enhanced StuDocu Downloader API v5.0 - Real-time Progress Tracking',
676
- version: '5.0.0',
677
  features: [
678
  'πŸͺ Advanced cookie banner bypass',
679
  'πŸ”“ Premium content unblurring',
@@ -682,8 +662,9 @@ app.get('/', (req, res) => {
682
  'πŸ“„ Clean PDF generation with print styles'
683
  ],
684
  endpoints: {
685
- download: 'POST /api/download (body: {url, filename?, email?, password?})',
686
  progress: 'GET /api/progress/:sessionId',
 
687
  health: 'GET /health'
688
  }
689
  });
@@ -700,6 +681,6 @@ process.on('SIGINT', () => {
700
  });
701
 
702
  app.listen(port, () => {
703
- console.log(`πŸš€ Enhanced StuDocu Downloader v5.0.0 running on http://localhost:${port}`);
704
  console.log(`✨ Features: Real-time progress tracking and enhanced user feedback`);
705
  });
 
9
  app.use(cors());
10
  app.use(express.json());
11
 
12
+ // --- Progress Tracking and Job Storage ---
13
+
14
+ // Stores the real-time progress of active jobs
15
+ const progressTrackers = new Map();
16
+ // Stores the final state and result (PDF buffer or error) of jobs
17
+ const downloadJobs = new Map();
18
+
19
  class ProgressTracker extends EventEmitter {
20
  constructor(sessionId) {
21
  super();
 
29
  this.progress = progress;
30
  this.status = status;
31
  this.message = message;
32
+ const update = {
33
  sessionId: this.sessionId,
34
  progress,
35
  status,
36
  message,
37
  timestamp: new Date().toISOString()
38
  };
39
+ this.emit('progress', update);
 
40
  console.log(`πŸ“Š [${this.sessionId}] ${progress}% - ${status}: ${message}`);
41
  }
42
  }
43
 
 
 
44
 
45
+ // --- Puppeteer Logic (Unchanged) ---
 
 
46
  const bypassCookiesAndRestrictions = async (page, progressTracker) => {
47
  progressTracker?.updateProgress(5, 'bypassing', 'Setting up cookie bypass...');
48
 
 
161
  progressTracker?.updateProgress(10, 'bypassing', 'Cookie bypass configured successfully');
162
  return true;
163
  };
 
 
 
 
164
  const unblurContent = async (page, progressTracker) => {
165
  progressTracker?.updateProgress(15, 'unblurring', 'Removing content restrictions...');
166
 
 
219
 
220
  progressTracker?.updateProgress(20, 'unblurring', 'Content restrictions removed');
221
  };
 
 
 
 
222
  const applyPrintStyles = async (page, progressTracker) => {
223
  progressTracker?.updateProgress(85, 'styling', 'Applying print styles...');
224
 
 
275
 
276
  progressTracker?.updateProgress(88, 'styling', 'Print styles applied successfully');
277
  };
 
 
 
 
278
  const studocuDownloader = async (url, options = {}, progressTracker = null) => {
279
  let browser;
280
  try {
 
539
  }
540
  };
541
 
542
+ // --- API Routes ---
543
 
544
+ /**
545
+ * [NEW] Endpoint to start the download process and get a session ID.
546
+ */
547
+ app.post('/api/request-download', (req, res) => {
548
+ const { url, email, password } = req.body;
549
+ if (!url || !url.includes('studocu.com')) {
 
 
 
 
 
550
  return res.status(400).json({ error: 'Please provide a valid StuDocu URL.' });
551
  }
552
 
553
+ const sessionId = Date.now().toString();
 
 
 
 
 
 
554
  const progressTracker = new ProgressTracker(sessionId);
555
+
556
  progressTrackers.set(sessionId, progressTracker);
557
+ downloadJobs.set(sessionId, { status: 'processing' });
558
 
559
+ console.log(`🎯 Processing request for: ${url} [Session: ${sessionId}]`);
560
 
561
+ // Respond to the client immediately with the session ID
562
+ res.json({ sessionId });
563
+
564
+ // --- Start the PDF generation in the background ---
565
+ studocuDownloader(url, { email, password }, progressTracker)
566
  .then(pdfBuffer => {
567
+ // Store the successful result
568
+ downloadJobs.set(sessionId, { status: 'completed', buffer: pdfBuffer });
569
+ progressTrackers.delete(sessionId); // Clean up live tracker
570
  })
571
  .catch(error => {
572
+ // Store the error
573
+ downloadJobs.set(sessionId, { status: 'error', message: error.message });
574
+ progressTrackers.delete(sessionId); // Clean up live tracker
 
 
 
 
 
 
 
 
 
 
575
  });
 
 
 
 
 
 
576
  });
577
 
578
+
579
+ /**
580
+ * [EXISTING] Endpoint for the client to poll for progress updates.
581
+ */
582
+ app.get('/api/progress/:sessionId', (req, res) => {
583
  const { sessionId } = req.params;
584
  const tracker = progressTrackers.get(sessionId);
585
 
586
+ if (tracker) {
587
+ // Job is in progress, return live data
588
+ return res.json({
589
+ sessionId,
590
+ progress: tracker.progress,
591
+ status: tracker.status,
592
+ message: tracker.message,
593
+ timestamp: new Date().toISOString()
594
+ });
595
  }
596
 
597
+ const job = downloadJobs.get(sessionId);
598
+ if (job) {
599
+ // Job is finished, return final state
600
+ if (job.status === 'completed') {
601
+ return res.json({ sessionId, progress: 100, status: 'completed', message: 'PDF generated successfully!' });
602
+ }
603
+ if (job.status === 'error') {
604
+ return res.json({ sessionId, progress: -1, status: 'error', message: job.message });
605
+ }
606
+ }
607
+
608
+ return res.status(404).json({ error: 'Session not found' });
 
 
 
 
 
 
 
 
609
  });
610
 
611
+
612
+ /**
613
+ * [NEW] Endpoint to download the final PDF file.
614
+ */
615
+ app.get('/api/download/:sessionId', (req, res) => {
616
  const { sessionId } = req.params;
617
+ const job = downloadJobs.get(sessionId);
618
 
619
+ if (!job) {
620
+ return res.status(404).json({ error: 'Download session not found or expired.' });
621
  }
622
 
623
+ if (job.status === 'processing') {
624
+ return res.status(400).json({ error: 'Download is still processing.' });
625
+ }
626
+
627
+ if (job.status === 'error') {
628
+ return res.status(500).json({ error: `Failed to generate PDF: ${job.message}` });
629
+ }
630
+
631
+ if (job.status === 'completed' && job.buffer) {
632
+ res.setHeader('Content-Type', 'application/pdf');
633
+ res.setHeader('Content-Disposition', 'attachment; filename=studocu-document.pdf');
634
+ res.send(job.buffer);
635
+ // Optional: Clean up the job after download to save memory
636
+ // downloadJobs.delete(sessionId);
637
+ } else {
638
+ res.status(500).json({ error: 'An unknown error occurred.' });
639
+ }
640
  });
641
 
642
 
643
+ // --- Health and Info Endpoints (Unchanged) ---
644
  app.get('/health', (req, res) => {
645
  res.json({
646
  status: 'healthy',
 
652
 
653
  app.get('/', (req, res) => {
654
  res.json({
655
+ message: 'πŸš€ Enhanced StuDocu Downloader API v5.1 - Real-time Progress Tracking',
656
+ version: '5.1.0',
657
  features: [
658
  'πŸͺ Advanced cookie banner bypass',
659
  'πŸ”“ Premium content unblurring',
 
662
  'πŸ“„ Clean PDF generation with print styles'
663
  ],
664
  endpoints: {
665
+ request: 'POST /api/request-download (body: {url, filename?, email?, password?})',
666
  progress: 'GET /api/progress/:sessionId',
667
+ download: 'GET /api/download/:sessionId',
668
  health: 'GET /health'
669
  }
670
  });
 
681
  });
682
 
683
  app.listen(port, () => {
684
+ console.log(`πŸš€ Enhanced StuDocu Downloader v5.1.0 running on http://localhost:${port}`);
685
  console.log(`✨ Features: Real-time progress tracking and enhanced user feedback`);
686
  });