diff --git a/README.md b/README.md index b564336..6177624 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,7 @@ Convert web articles to PDF using the [Reader View](https://chromewebstore.googl npm install ``` -Extract the Reader View extension (get it using [CRX Downloader](https://chromewebstore.google.com/detail/crx-extractordownloader/ajkhmmldknmfjnmeedkbkkojgobmljda)): - -```bash -mkdir -p extension/reader-view -unzip extension/Reader-View-Chrome-Web-Store.zip -d extension/reader-view -``` +The Reader View extension is downloaded and extracted automatically on first run. ## Usage diff --git a/src/browser.js b/src/browser.js index e461d45..26235d6 100644 --- a/src/browser.js +++ b/src/browser.js @@ -1,26 +1,31 @@ import fs from 'node:fs' import path from 'node:path' import puppeteer from 'puppeteer' +import { downloadExtension } from './download-extension.js' import logger from './logger.js' const EXTENSION_DIR = path.resolve('./extension/reader-view') const MANIFEST_PATH = path.join(EXTENSION_DIR, 'manifest.json') const READABILITY_PATH = path.join(EXTENSION_DIR, 'data/inject/Readability.js') -export function checkExtension () { +export async function checkExtension () { if (!fs.existsSync(MANIFEST_PATH)) { - logger.error( - `Extension not found at: "${EXTENSION_DIR}"\n` + - 'Please extract the extension first:\n' + - ' mkdir -p extension/reader-view\n' + - ' unzip extension/Reader-View-Chrome-Web-Store.zip -d extension/reader-view' - ) - process.exit(1) + logger.info('Extension not found. Downloading automatically...') + try { + await downloadExtension({ logger }) + } catch (err) { + logger.error(`Failed to download extension: ${err.message}`) + process.exit(1) + } + if (!fs.existsSync(MANIFEST_PATH)) { + logger.error('Download completed but extension not found. Extraction may have failed.') + process.exit(1) + } } if (!fs.existsSync(READABILITY_PATH)) { logger.error( `Readability.js not found at: "${READABILITY_PATH}"\n` + - 'The extension may be incomplete. Please re-extract it.' + 'The extension may be incomplete.' ) process.exit(1) } diff --git a/src/download-extension.js b/src/download-extension.js new file mode 100644 index 0000000..e69f9b7 --- /dev/null +++ b/src/download-extension.js @@ -0,0 +1,84 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { execSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import process from 'node:process' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const EXTENSION_DIR = path.resolve(__dirname, '..', 'extension') +const EXTENSION_ID = 'ecabifbgmdmgdllomnfinbmaellmclnh' +const CRX_PATH = path.join(EXTENSION_DIR, 'Reader-View-Chrome-Web-Store.crx') +const ZIP_PATH = path.join(EXTENSION_DIR, 'Reader-View-Chrome-Web-Store.zip') +const EXTRACT_DIR = path.join(EXTENSION_DIR, 'reader-view') + +export async function downloadExtension ({ logger } = {}) { + const log = logger || console + + log.info('Downloading Reader View extension from Chrome Web Store...') + + const url = + 'https://clients2.google.com/service/update2/crx' + + '?response=redirect' + + '&prodversion=150.0.0.0' + + `&x=id%3D${EXTENSION_ID}%26installsource%3Dondemand%26uc` + + '&nacl_arch=arm' + + '&acceptformat=crx3' + + const response = await fetch(url, { redirect: 'follow' }) + if (!response.ok) { + throw new Error(`Failed to download CRX: HTTP ${response.status}`) + } + + const crx = new Uint8Array(await response.arrayBuffer()) + log.info(`Downloaded ${(crx.length / 1024).toFixed(0)} KB`) + + if (crx[0] !== 0x43 || crx[1] !== 0x72 || crx[2] !== 0x32 || crx[3] !== 0x34) { + throw new Error('Not a valid CRX file (bad magic number)') + } + + const view = new DataView(crx.buffer, crx.byteOffset, crx.byteLength) + const version = view.getUint32(4, true) + + let zipOffset + if (version === 2) { + const publicKeyLength = view.getUint32(8, true) + const signatureLength = view.getUint32(12, true) + zipOffset = 16 + publicKeyLength + signatureLength + } else if (version === 3) { + const headerLength = view.getUint32(8, true) + zipOffset = 12 + headerLength + } else { + throw new Error(`Unsupported CRX version: ${version}`) + } + + const zip = crx.slice(zipOffset) + if (zip[0] !== 0x50 || zip[1] !== 0x4b) { + throw new Error('Failed to locate ZIP payload in CRX') + } + + await fs.mkdir(EXTENSION_DIR, { recursive: true }) + await fs.writeFile(CRX_PATH, crx) + await fs.writeFile(ZIP_PATH, zip) + + log.info('Extracting extension...') + await fs.mkdir(EXTRACT_DIR, { recursive: true }) + + try { + execSync(`unzip -o "${ZIP_PATH}" -d "${EXTRACT_DIR}"`, { stdio: 'pipe' }) + } catch { + throw new Error( + 'Failed to extract the extension. Make sure "unzip" is installed.\n' + + `You can manually extract it: unzip "${ZIP_PATH}" -d "${EXTRACT_DIR}"` + ) + } + + log.info('Extension ready.') +} + +const __filename = fileURLToPath(import.meta.url) +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { + downloadExtension().catch((err) => { + console.error('Download failed:', err.message) + process.exit(1) + }) +} diff --git a/src/index.js b/src/index.js index 4255b17..00a954c 100644 --- a/src/index.js +++ b/src/index.js @@ -41,7 +41,7 @@ async function promptForInput () { async function main () { const { url, opts } = parseArgs() - checkExtension() + await checkExtension() let preferences = {} if (opts.prefs) {