|
| 1 | +#!/usr/bin/env node |
| 2 | +const fs = require('fs'); |
| 3 | +const os = require('os'); |
| 4 | +const path = require('path'); |
| 5 | +const readline = require('readline'); |
| 6 | +const puppeteer = require('puppeteer'); |
| 7 | +const { PDFDocument } = require('pdf-lib'); |
| 8 | +const { initializeApp, cert } = require('firebase-admin/app'); |
| 9 | +const { getFirestore } = require('firebase-admin/firestore'); |
| 10 | + |
| 11 | +const OUTPUT_ROOT = path.resolve(__dirname, 'geenrated-result'); |
| 12 | +const BASE_URL = 'https://pcks.devflex.co.in'; |
| 13 | + |
| 14 | +// Reuse the existing admin SDK credential from the desktop app |
| 15 | +const serviceAccount = require('../desktop/src/utils/service_key.json'); |
| 16 | + |
| 17 | +initializeApp({ |
| 18 | + credential: cert(serviceAccount), |
| 19 | +}); |
| 20 | + |
| 21 | +const DB = getFirestore(); |
| 22 | +DB.settings({ ignoreUndefinedProperties: true }); |
| 23 | + |
| 24 | +function log(message) { |
| 25 | + console.log(`[PCKS CLI] ${message}`); |
| 26 | +} |
| 27 | + |
| 28 | +function promptUser() { |
| 29 | + const rl = readline.createInterface({ |
| 30 | + input: process.stdin, |
| 31 | + output: process.stdout, |
| 32 | + }); |
| 33 | + |
| 34 | + const ask = (question) => |
| 35 | + new Promise((resolve) => |
| 36 | + rl.question(question, (answer) => resolve(answer.trim())) |
| 37 | + ); |
| 38 | + |
| 39 | + return (async () => { |
| 40 | + log('Welcome to PCKS result downloader (CLI)'); |
| 41 | + const year = await ask('Enter year (e.g. 2023): '); |
| 42 | + |
| 43 | + const termMap = { |
| 44 | + 1: 'first', |
| 45 | + 2: 'annual', |
| 46 | + 3: 'second', |
| 47 | + }; |
| 48 | + |
| 49 | + log('Select term:'); |
| 50 | + log(' 1) First'); |
| 51 | + log(' 2) Annual'); |
| 52 | + log(' 3) Second'); |
| 53 | + const termChoice = await ask('Choice (1/2/3): '); |
| 54 | + rl.close(); |
| 55 | + |
| 56 | + const term = termMap[termChoice] || termChoice.toLowerCase(); |
| 57 | + |
| 58 | + if (!year || !term || !['first', 'annual', 'second'].includes(term)) { |
| 59 | + throw new Error( |
| 60 | + 'Invalid input. Please provide a year and a valid term (first/annual/second).' |
| 61 | + ); |
| 62 | + } |
| 63 | + |
| 64 | + return { year, term }; |
| 65 | + })(); |
| 66 | +} |
| 67 | + |
| 68 | +async function fetchAdmissions(year, term) { |
| 69 | + const collectionPath = `results/${year}/${term}`; |
| 70 | + log(`Fetching admissions from Firestore at ${collectionPath}...`); |
| 71 | + const snapshot = await DB.collection(collectionPath).get(); |
| 72 | + |
| 73 | + if (snapshot.empty) { |
| 74 | + throw new Error('No results found for the provided year and term.'); |
| 75 | + } |
| 76 | + |
| 77 | + const results = snapshot.docs.map((doc) => ({ |
| 78 | + id: doc.id, |
| 79 | + isCompleted: doc.data().isCompleted, |
| 80 | + })); |
| 81 | + |
| 82 | + console.log(results); |
| 83 | + |
| 84 | + const completed = results.filter((item) => item.isCompleted); |
| 85 | + log( |
| 86 | + `Found ${results.length} records, ${completed.length} marked as completed.` |
| 87 | + ); |
| 88 | + if (!completed.length) { |
| 89 | + throw new Error('No completed results found to download.'); |
| 90 | + } |
| 91 | + |
| 92 | + return completed; |
| 93 | +} |
| 94 | + |
| 95 | +async function downloadAndMerge({ year, term, admissions }) { |
| 96 | + const runDir = path.join(OUTPUT_ROOT, `${year}-${term}-${Date.now()}`); |
| 97 | + fs.mkdirSync(runDir, { recursive: true }); |
| 98 | + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pcks-cli-')); |
| 99 | + log(`Using temp directory: ${tempDir}`); |
| 100 | + log(`Output directory: ${runDir}`); |
| 101 | + |
| 102 | + let browser; |
| 103 | + let page; |
| 104 | + const downloaded = []; |
| 105 | + |
| 106 | + try { |
| 107 | + browser = await puppeteer.launch(); |
| 108 | + page = await browser.newPage(); |
| 109 | + page.setDefaultNavigationTimeout(60_000); |
| 110 | + |
| 111 | + for (let index = 0; index < admissions.length; index++) { |
| 112 | + const admissionNo = admissions[index].id; |
| 113 | + const url = `${BASE_URL}/dashboard/result/view?batch=${year}&term=${term}&admissionNo=${admissionNo}`; |
| 114 | + log(`(${index + 1}/${admissions.length}) Fetching ${url}`); |
| 115 | + await page.goto(url, { waitUntil: 'networkidle2' }); |
| 116 | + // await page.waitForTimeout(1_000); |
| 117 | + const pdfPath = path.join(tempDir, `${admissionNo}.pdf`); |
| 118 | + await page.emulateMediaType('print'); |
| 119 | + await page.pdf({ |
| 120 | + path: pdfPath, |
| 121 | + format: 'A4', |
| 122 | + margin: { top: '15mm', right: '15mm', bottom: '15mm', left: '15mm' }, |
| 123 | + }); |
| 124 | + downloaded.push(pdfPath); |
| 125 | + log(`Saved PDF for ${admissionNo} -> ${pdfPath}`); |
| 126 | + } |
| 127 | + |
| 128 | + log('Merging PDFs...'); |
| 129 | + const mergedPdf = await PDFDocument.create(); |
| 130 | + for (let idx = 0; idx < downloaded.length; idx++) { |
| 131 | + const pdfPath = downloaded[idx]; |
| 132 | + const pdfBytes = fs.readFileSync(pdfPath); |
| 133 | + const pdf = await PDFDocument.load(pdfBytes); |
| 134 | + const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); |
| 135 | + copiedPages.forEach((page) => mergedPdf.addPage(page)); |
| 136 | + log(`Merged ${idx + 1}/${downloaded.length}`); |
| 137 | + } |
| 138 | + |
| 139 | + const mergedPdfBytes = await mergedPdf.save(); |
| 140 | + const mergedPath = path.join(runDir, `results-${year}-${term}.pdf`); |
| 141 | + fs.writeFileSync(mergedPath, mergedPdfBytes); |
| 142 | + log(`Merged PDF saved at ${mergedPath}`); |
| 143 | + |
| 144 | + // Optionally keep individual PDFs alongside merged output for auditing |
| 145 | + downloaded.forEach((pdfPath) => { |
| 146 | + const dest = path.join(runDir, path.basename(pdfPath)); |
| 147 | + fs.copyFileSync(pdfPath, dest); |
| 148 | + }); |
| 149 | + log('Copied individual PDFs to output directory.'); |
| 150 | + |
| 151 | + return { mergedPath, runDir, tempDir, downloaded }; |
| 152 | + } finally { |
| 153 | + if (browser) { |
| 154 | + try { |
| 155 | + await browser.close(); |
| 156 | + } catch (error) { |
| 157 | + log(`Error closing browser: ${error.message}`); |
| 158 | + } |
| 159 | + } |
| 160 | + // Clean temp files |
| 161 | + try { |
| 162 | + downloaded.forEach((file) => fs.rmSync(file, { force: true })); |
| 163 | + fs.rmSync(tempDir, { recursive: true, force: true }); |
| 164 | + } catch (error) { |
| 165 | + log(`Error cleaning up temp directory: ${error.message}`); |
| 166 | + } |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +async function run() { |
| 171 | + try { |
| 172 | + const { year, term } = await promptUser(); |
| 173 | + const admissions = await fetchAdmissions(year, term); |
| 174 | + await downloadAndMerge({ year, term, admissions }); |
| 175 | + log('All done. Check the geenrated-result folder for outputs.'); |
| 176 | + } catch (error) { |
| 177 | + log(`Error: ${error.message || error}`); |
| 178 | + process.exitCode = 1; |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +run(); |
0 commit comments