UNPKG

pdown

Version:

Command-line tool for downloading files from Proton Drive

111 lines (110 loc) 4.14 kB
#!/usr/bin/env node import { program } from 'commander'; import packageJSON from './package.json' with { type: 'json' }; import fs from 'fs'; import path from 'path'; import puppeteer from 'puppeteer'; const selectors = { download: 'button[data-testid="download-button"]', downloadOptionDropdown: 'button[data-testid="dropdown-download-button"]', downloadProgress: '.transfers-manager-list-item-size', downloadFilename: '.transfers-manager-list-item-name' }; program .name('pdown') .description('CLI tool for downloading files from Proton Drive') .usage('[options] <url>') .version(packageJSON.version) .option('-s, --speed <kbps>', 'limit download speed (in kilobytes per second)', parseInt) .option('-p, --path <downloadPath>', 'set download folder path', './downloads') .option('-u, --user-agent <userAgent>', 'override default user agent') .option('-c, --cookies <cookieFile>', 'path to a Netscape cookie file') .argument('[items...]', 'URLs/IDs of the Proton Drive files/folders to download') .parse(process.argv); const options = program.opts(); const args = program.args.filter(str => str.trim().length > 0); const urls = new Set(); args.forEach(arg => { switch (true) { case arg.startsWith('https://drive.proton.me/urls/'): urls.add(arg); break; case /^\w{10}#\w{12}$/.test(arg): urls.add(`https://drive.proton.me/urls/${arg}`); break; default: console.error(`Skipping invalid URL/ID: ${arg}`); break; } }); console.log(`Downloading: ${Array.from(urls).join(', ')}`); if (urls.size === 0) { console.error('At least one valid URL/ID is required.'); program.help({ error: true }); } if (!options.path) { options.path = '~/Downloads'; } (async () => { const downloadPath = path.resolve(options.path); if (!fs.existsSync(downloadPath)) { fs.mkdirSync(downloadPath, { recursive: true }); } const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] }); const page = await browser.newPage(); if (options.userAgent) { await page.setUserAgent(options.userAgent); } if (options.cookies) { // todo } if (options.speed) { const bytesPerSecond = options.speed * 1024; await page.emulateNetworkConditions({ download: bytesPerSecond, upload: bytesPerSecond, latency: 10 }); console.log(`Throttling network to ${options.speed} KB/s`); } const client = await page.createCDPSession(); await client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath }); for (const url of urls) { await page.goto(url, { waitUntil: 'networkidle0' }); await page.locator(selectors['downloadOptionDropdown']).click(); await page.locator(selectors['download']).click(); const filename = await page.$eval(selectors['downloadFilename'], el => { const innerSpan = el.querySelector('span span[aria-label]'); return innerSpan ? innerSpan.getAttribute('aria-label') : null; }); let downloadComplete = false; while (!downloadComplete) { const progress = await page.$eval(selectors['downloadProgress'], el => { if (!el) { return { percent: '', text: '' }; } return { percent: el.getAttribute('title')?.trim(), text: el.textContent?.trim() }; }); process.stdout.write(`${filename}: ${progress.percent} ${progress.text}\r`); if (progress.percent === '100%') { downloadComplete = true; } else { await new Promise(resolve => setTimeout(resolve, 500)); } } } ; for (const file of fs.readdirSync(downloadPath)) { if (file.endsWith('.crdownload')) { fs.unlinkSync(path.join(downloadPath, file)); } } await browser.close(); })();